Merge pull request #9168 from backstage/blam/scaffodler-secrets-frontend
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user