Merge pull request #9168 from backstage/blam/scaffodler-secrets-frontend

This commit is contained in:
Ben Lambert
2022-02-03 10:23:05 +01:00
committed by GitHub
34 changed files with 922 additions and 62 deletions
+7
View File
@@ -29,6 +29,7 @@ export class ScmAuth implements ScmAuthApi {
ProfileInfoApi &
BackstageIdentityApi &
SessionApi;
bitbucket: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi;
}
>;
static forAuthApi(
@@ -82,6 +83,12 @@ export const scmAuthApiRef: ApiRef<ScmAuthApi>;
export interface ScmAuthTokenOptions extends AuthRequestOptions {
additionalScope?: {
repoWrite?: boolean;
customScopes?: {
github?: string[];
azure?: string[];
bitbucket?: string[];
gitlab?: string[];
};
};
url: string;
}
@@ -141,6 +141,58 @@ describe('ScmAuth', () => {
});
});
it('should support additional provided scopes from the caller', async () => {
const mockAuthApi = {
getAccessToken: async (scopes: string[]) => {
return scopes.join(' ');
},
};
const githubAuth = ScmAuth.forGithub(mockAuthApi);
await expect(
githubAuth.getCredentials({
url: 'http://example.com',
additionalScope: {
customScopes: { github: ['org:read', 'workflow:write'] },
},
}),
).resolves.toMatchObject({
token: 'repo read:org read:user org:read workflow:write',
});
const gitlabAuth = ScmAuth.forGitlab(mockAuthApi);
await expect(
gitlabAuth.getCredentials({
url: 'http://example.com',
additionalScope: { customScopes: { gitlab: ['write_repository'] } },
}),
).resolves.toMatchObject({
token: 'read_user read_api read_repository write_repository',
});
const azureAuth = ScmAuth.forAzure(mockAuthApi);
await expect(
azureAuth.getCredentials({
url: 'http://example.com',
additionalScope: { customScopes: { azure: ['vso.org'] } },
}),
).resolves.toMatchObject({
token: 'vso.build vso.code vso.graph vso.project vso.profile vso.org',
});
const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi);
await expect(
bitbucketAuth.getCredentials({
url: 'http://example.com',
additionalScope: {
customScopes: { bitbucket: ['snippet:write', 'issue:write'] },
},
}),
).resolves.toMatchObject({
token: 'account team pullrequest snippet issue snippet:write issue:write',
});
});
it('should handle host option', () => {
const mockAuthApi = {
getAccessToken: jest.fn(),
+41 -8
View File
@@ -15,6 +15,7 @@
*/
import {
bitbucketAuthApiRef,
createApiFactory,
githubAuthApiRef,
gitlabAuthApiRef,
@@ -35,6 +36,9 @@ type ScopeMapping = {
repoWrite: string[];
};
// An enum of all supported providers
type ProviderName = 'generic' | 'github' | 'azure' | 'bitbucket' | 'gitlab';
class ScmAuthMux implements ScmAuthApi {
#providers: Array<ScmAuth>;
@@ -93,12 +97,14 @@ export class ScmAuth implements ScmAuthApi {
github: githubAuthApiRef,
gitlab: gitlabAuthApiRef,
azure: microsoftAuthApiRef,
bitbucket: bitbucketAuthApiRef,
},
factory: ({ github, gitlab, azure }) =>
factory: ({ github, gitlab, azure, bitbucket }) =>
ScmAuth.merge(
ScmAuth.forGithub(github),
ScmAuth.forGitlab(gitlab),
ScmAuth.forAzure(azure),
ScmAuth.forBitbucket(bitbucket),
),
});
}
@@ -116,7 +122,7 @@ export class ScmAuth implements ScmAuthApi {
};
},
): ScmAuth {
return new ScmAuth(authApi, options.host, options.scopeMapping);
return new ScmAuth('generic', authApi, options.host, options.scopeMapping);
}
/**
@@ -139,7 +145,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'github.com';
return new ScmAuth(githubAuthApi, host, {
return new ScmAuth('github', githubAuthApi, host, {
default: ['repo', 'read:org', 'read:user'],
repoWrite: ['gist'],
});
@@ -165,7 +171,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'gitlab.com';
return new ScmAuth(gitlabAuthApi, host, {
return new ScmAuth('gitlab', gitlabAuthApi, host, {
default: ['read_user', 'read_api', 'read_repository'],
repoWrite: ['write_repository', 'api'],
});
@@ -191,7 +197,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'dev.azure.com';
return new ScmAuth(microsoftAuthApi, host, {
return new ScmAuth('azure', microsoftAuthApi, host, {
default: [
'vso.build',
'vso.code',
@@ -223,7 +229,7 @@ export class ScmAuth implements ScmAuthApi {
},
): ScmAuth {
const host = options?.host ?? 'bitbucket.org';
return new ScmAuth(bitbucketAuthApi, host, {
return new ScmAuth('bitbucket', bitbucketAuthApi, host, {
default: ['account', 'team', 'pullrequest', 'snippet', 'issue'],
repoWrite: ['pullrequest:write', 'snippet:write', 'issue:write'],
});
@@ -240,11 +246,18 @@ export class ScmAuth implements ScmAuthApi {
#api: OAuthApi;
#host: string;
#scopeMapping: ScopeMapping;
#providerName: ProviderName;
private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) {
private constructor(
providerName: ProviderName,
api: OAuthApi,
host: string,
scopeMapping: ScopeMapping,
) {
this.#api = api;
this.#host = host;
this.#scopeMapping = scopeMapping;
this.#providerName = providerName;
}
/**
@@ -254,6 +267,16 @@ export class ScmAuth implements ScmAuthApi {
return url.host === this.#host;
}
private getAdditionalScopesForProvider(
additionalScopes: ScmAuthTokenOptions['additionalScope'],
): string[] {
if (!additionalScopes?.customScopes || this.#providerName === 'generic') {
return [];
}
return additionalScopes.customScopes?.[this.#providerName] ?? [];
}
/**
* Fetches credentials for the given resource.
*/
@@ -267,7 +290,17 @@ export class ScmAuth implements ScmAuthApi {
scopes.push(...this.#scopeMapping.repoWrite);
}
const token = await this.#api.getAccessToken(scopes, restOptions);
const additionalScopes =
this.getAdditionalScopesForProvider(additionalScope);
if (additionalScopes.length) {
scopes.push(...additionalScopes);
}
const uniqueScopes = [...new Set(scopes)];
const token = await this.#api.getAccessToken(uniqueScopes, restOptions);
return {
token,
headers: {
@@ -44,6 +44,16 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions {
* the ability to create things like issues and pull requests.
*/
repoWrite?: boolean;
/**
* Allow an arbitrary list of scopes provided from the user
* to request from the provider.
*/
customScopes?: {
github?: string[];
azure?: string[];
bitbucket?: string[];
gitlab?: string[];
};
};
}