Skip to content

Commit

Permalink
Enforce minimum scopes in the UI (#20217)
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptronicek authored Sep 16, 2024
1 parent 4b05e71 commit 35f53b9
Show file tree
Hide file tree
Showing 24 changed files with 155 additions and 291 deletions.
111 changes: 58 additions & 53 deletions components/dashboard/src/user-settings/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* See License.AGPL.txt in the project root for license information.
*/

import { getScopesForAuthProviderType } from "@gitpod/public-api-common/lib/auth-providers";
import { getRequiredScopes, getScopesForAuthProviderType } from "@gitpod/public-api-common/lib/auth-providers";
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
import { useQuery } from "@tanstack/react-query";
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import Alert from "../components/Alert";
import { CheckboxInputField, CheckboxListField } from "../components/forms/CheckboxInputField";
import ConfirmationModal from "../components/ConfirmationModal";
Expand Down Expand Up @@ -54,6 +54,46 @@ export default function Integrations() {
);
}

const getDescriptionForScope = (scope: string) => {
switch (scope) {
// GitHub
case "user:email":
return "Read-only access to your email addresses";
case "read:user":
return "Read-only access to your profile information";
case "public_repo":
return "Write access to code in public repositories and organizations";
case "repo":
return "Read/write access to code in private repositories and organizations";
case "read:org":
return "Read-only access to organizations (used to suggest organizations when forking a repository)";
case "workflow":
return "Allow updating GitHub Actions workflow files";
// GitLab
case "read_user":
return "Read-only access to your email addresses";
case "api":
return "Allow making API calls (used to set up a webhook when enabling prebuilds for a repository)";
case "read_repository":
return "Read/write access to your repositories";
// Bitbucket
case "account":
return "Read-only access to your account information";
case "repository":
return "Read-only access to your repositories (note: Bitbucket doesn't support revoking scopes)";
case "repository:write":
return "Read/write access to your repositories (note: Bitbucket doesn't support revoking scopes)";
case "pullrequest":
return "Read access to pull requests and ability to collaborate via comments, tasks, and approvals (note: Bitbucket doesn't support revoking scopes)";
case "pullrequest:write":
return "Allow creating, merging and declining pull requests (note: Bitbucket doesn't support revoking scopes)";
case "webhook":
return "Allow installing webhooks (used when enabling prebuilds for a repository, note: Bitbucket doesn't support revoking scopes)";
default:
return "";
}
};

function GitProviders() {
const { user, setUser } = useContext(UserContext);

Expand Down Expand Up @@ -250,45 +290,6 @@ function GitProviders() {
setEditModal({ ...editModal, nextScopes });
};

const getDescriptionForScope = (scope: string) => {
switch (scope) {
case "user:email":
return "Read-only access to your email addresses";
case "read:user":
return "Read-only access to your profile information";
case "public_repo":
return "Write access to code in public repositories and organizations";
case "repo":
return "Read/write access to code in private repositories and organizations";
case "read:org":
return "Read-only access to organizations (used to suggest organizations when forking a repository)";
case "workflow":
return "Allow updating GitHub Actions workflow files";
// GitLab
case "read_user":
return "Read-only access to your email addresses";
case "api":
return "Allow making API calls (used to set up a webhook when enabling prebuilds for a repository)";
case "read_repository":
return "Read/write access to your repositories";
// Bitbucket
case "account":
return "Read-only access to your account information";
case "repository":
return "Read-only access to your repositories (note: Bitbucket doesn't support revoking scopes)";
case "repository:write":
return "Read/write access to your repositories (note: Bitbucket doesn't support revoking scopes)";
case "pullrequest":
return "Read access to pull requests and ability to collaborate via comments, tasks, and approvals (note: Bitbucket doesn't support revoking scopes)";
case "pullrequest:write":
return "Allow creating, merging and declining pull requests (note: Bitbucket doesn't support revoking scopes)";
case "webhook":
return "Allow installing webhooks (used when enabling prebuilds for a repository, note: Bitbucket doesn't support revoking scopes)";
default:
return "";
}
};

return (
<div>
{selectAccountModal && (
Expand Down Expand Up @@ -325,18 +326,22 @@ function GitProviders() {
<ModalHeader>Edit Permissions</ModalHeader>
<ModalBody>
<CheckboxListField label="Configure provider permissions.">
{(getScopesForAuthProviderType(editModal.provider.type) || []).map((scope) => (
<CheckboxInputField
key={scope}
value={scope}
label={scope}
hint={getDescriptionForScope(scope)}
checked={editModal.nextScopes.has(scope)}
// disabled={editModal.provider.requirements?.default.includes(scope)} // what?!
topMargin={false}
onChange={(checked) => onChangeScopeHandler(checked, scope)}
/>
))}
{(getScopesForAuthProviderType(editModal.provider.type) || []).map((scope) => {
const isRequired = getRequiredScopes(editModal.provider.type)?.default.includes(scope);

return (
<CheckboxInputField
key={scope}
value={scope}
label={scope + (isRequired ? " (required)" : "")}
hint={getDescriptionForScope(scope)}
checked={editModal.nextScopes.has(scope)}
disabled={isRequired}
topMargin={false}
onChange={(checked) => onChangeScopeHandler(checked, scope)}
/>
);
})}
</CheckboxListField>
</ModalBody>
<ModalFooter>
Expand Down
2 changes: 1 addition & 1 deletion components/gitpod-db/src/typeorm/user-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl<UserDB> implements Us
accessTokenExpiresAt: expiry,
client,
user,
scopes: scopes,
scopes,
};
}
async issueRefreshToken(accessToken: OAuthToken): Promise<OAuthToken> {
Expand Down
84 changes: 50 additions & 34 deletions components/public-api/typescript-common/src/auth-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@

import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";

export namespace GitLabScope {
export namespace GitLabOAuthScopes {
export const READ_USER = "read_user";
export const API = "api";
export const READ_REPO = "read_repository";

export const ALL = [READ_USER, API, READ_REPO];
/**
* Minimal required permission.
* GitLab API usage requires the permission of a user.
*/
export const DEFAULT = [READ_USER, API];
export const REPO = [API, READ_REPO];

export const Requirements = {
/**
* Minimal required permission.
* GitLab API usage requires the permission of a user.
*/
DEFAULT: [READ_USER, API],

REPO: [API, READ_REPO],
};
}

export namespace GitHubScope {
export namespace GitHubOAuthScopes {
export const EMAIL = "user:email";
export const READ_USER = "read:user";
export const PUBLIC = "public_repo";
Expand All @@ -29,9 +33,17 @@ export namespace GitHubScope {
export const WORKFLOW = "workflow";

export const ALL = [EMAIL, READ_USER, PUBLIC, PRIVATE, ORGS, WORKFLOW];
export const DEFAULT = ALL;
export const PUBLIC_REPO = ALL;
export const PRIVATE_REPO = ALL;

export const Requirements = {
/**
* Minimal required permission.
* GitHub's API is not restricted any further.
*/
DEFAULT: [EMAIL],

PUBLIC_REPO: [PUBLIC],
PRIVATE_REPO: [PRIVATE],
};
}

export namespace BitbucketOAuthScopes {
Expand All @@ -48,15 +60,14 @@ export namespace BitbucketOAuthScopes {
/** Create, comment and merge pull requests */
export const PULL_REQUEST_WRITE = "pullrequest:write";

export const ALL = [
ACCOUNT_READ,
REPOSITORY_READ,
REPOSITORY_WRITE,
PULL_REQUEST_READ,
PULL_REQUEST_WRITE,
];
export const ALL = [ACCOUNT_READ, REPOSITORY_READ, REPOSITORY_WRITE, PULL_REQUEST_READ, PULL_REQUEST_WRITE];

export const DEFAULT = ALL;
export const Requirements = {
/**
* Minimal required permission.
*/
DEFAULT: ALL,
};
}

export namespace BitbucketServerOAuthScopes {
Expand All @@ -74,17 +85,22 @@ export namespace BitbucketServerOAuthScopes {

export const ALL = [PUBLIC_REPOS, REPO_READ, REPO_WRITE, REPO_ADMIN, PROJECT_ADMIN];

export const DEFAULT = ALL;
export const Requirements = {
/**
* Minimal required permission.
*/
DEFAULT: [PUBLIC_REPOS, REPO_READ, REPO_WRITE],
};
}

export function getScopesForAuthProviderType(type: AuthProviderType | string) {
switch (type) {
case AuthProviderType.GITHUB:
case "GitHub":
return GitHubScope.ALL;
return GitHubOAuthScopes.ALL;
case AuthProviderType.GITLAB:
case "GitLab":
return GitLabScope.ALL;
return GitLabOAuthScopes.ALL;
case AuthProviderType.BITBUCKET:
case "Bitbucket":
return BitbucketOAuthScopes.ALL;
Expand All @@ -99,30 +115,30 @@ export function getRequiredScopes(type: AuthProviderType | string) {
case AuthProviderType.GITHUB:
case "GitHub":
return {
default: GitHubScope.DEFAULT,
publicRepo: GitHubScope.PUBLIC_REPO,
privateRepo: GitHubScope.PRIVATE_REPO,
default: GitHubOAuthScopes.Requirements.DEFAULT,
publicRepo: GitHubOAuthScopes.Requirements.PUBLIC_REPO,
privateRepo: GitHubOAuthScopes.Requirements.PRIVATE_REPO,
};
case AuthProviderType.GITLAB:
case "GitLab":
return {
default: GitLabScope.DEFAULT,
publicRepo: GitLabScope.DEFAULT,
privateRepo: GitLabScope.REPO,
default: GitLabOAuthScopes.Requirements.DEFAULT,
publicRepo: GitLabOAuthScopes.Requirements.DEFAULT,
privateRepo: GitLabOAuthScopes.Requirements.REPO,
};
case AuthProviderType.BITBUCKET:
case "Bitbucket":
return {
default: BitbucketOAuthScopes.DEFAULT,
publicRepo: BitbucketOAuthScopes.DEFAULT,
privateRepo: BitbucketOAuthScopes.DEFAULT,
default: BitbucketOAuthScopes.Requirements.DEFAULT,
publicRepo: BitbucketOAuthScopes.Requirements.DEFAULT,
privateRepo: BitbucketOAuthScopes.Requirements.DEFAULT,
};
case AuthProviderType.BITBUCKET_SERVER:
case "BitbucketServer":
return {
default: BitbucketServerOAuthScopes.DEFAULT,
publicRepo: BitbucketServerOAuthScopes.DEFAULT,
privateRepo: BitbucketServerOAuthScopes.DEFAULT,
default: BitbucketServerOAuthScopes.Requirements.DEFAULT,
publicRepo: BitbucketServerOAuthScopes.Requirements.DEFAULT,
privateRepo: BitbucketServerOAuthScopes.Requirements.DEFAULT,
};
}
}
51 changes: 0 additions & 51 deletions components/server/src/auth/auth-provider-scopes.ts

This file was deleted.

6 changes: 3 additions & 3 deletions components/server/src/auth/auth-provider-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import fetch from "node-fetch";
import { Authorizer } from "../authorization/authorizer";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { getRequiredScopes, getScopesOfProvider } from "./auth-provider-scopes";
import { getRequiredScopes, getScopesForAuthProviderType } from "@gitpod/public-api-common/lib/auth-providers";

@injectable()
export class AuthProviderService {
Expand Down Expand Up @@ -106,8 +106,8 @@ export class AuthProviderService {
hiddenOnDashboard: ap.hiddenOnDashboard,
disallowLogin: ap.disallowLogin,
description: ap.description,
scopes: getScopesOfProvider(ap),
requirements: getRequiredScopes(ap),
scopes: getScopesForAuthProviderType(ap.type),
requirements: getRequiredScopes(ap.type),
};
}

Expand Down
4 changes: 2 additions & 2 deletions components/server/src/auth/resource-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { UnauthorizedError } from "../errors";
import { RepoURL } from "../repohost";
import { HostContextProvider } from "./host-context-provider";
import { reportGuardAccessCheck } from "../prometheus-metrics";
import { getRequiredScopes } from "./auth-provider-scopes";
import { FunctionAccessGuard } from "./function-access";
import { getRequiredScopes } from "@gitpod/public-api-common/lib/auth-providers";

declare let resourceInstance: GuardedResource;
export type GuardedResourceKind = typeof resourceInstance.kind;
Expand Down Expand Up @@ -585,7 +585,7 @@ export class RepositoryResourceGuard implements ResourceAccessGuard {
const identity = User.getIdentity(this.user, authProvider.authProviderId);
if (!identity) {
const providerType = authProvider.info.authProviderType;
const requiredScopes = getRequiredScopes({ type: providerType })?.default;
const requiredScopes = getRequiredScopes(providerType)?.default;
throw UnauthorizedError.create({
host: repoUrl.host,
repoName: repoUrl.repo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import express from "express";
import { inject, injectable } from "inversify";
import { AuthUserSetup } from "../auth/auth-provider";
import { GenericAuthProvider } from "../auth/generic-auth-provider";
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
import { BitbucketServerApi } from "./bitbucket-server-api";
import { BitbucketServerOAuthScopes } from "@gitpod/public-api-common/lib/auth-providers";

@injectable()
export class BitbucketServerAuthProvider extends GenericAuthProvider {
Expand Down
Loading

0 comments on commit 35f53b9

Please sign in to comment.