Add resolveCatalogMemberClaims method
Signed-off-by: Tim Hansen <timbonicus@gmail.com>
This commit is contained in:
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import {
|
||||
RELATION_MEMBER_OF,
|
||||
UserEntity,
|
||||
UserEntityV1alpha1,
|
||||
} from '@backstage/catalog-model';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { CatalogIdentityClient } from './CatalogIdentityClient';
|
||||
|
||||
@@ -37,12 +41,12 @@ describe('CatalogIdentityClient', () => {
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
it('passes through the correct search params', async () => {
|
||||
it('findUser passes through the correct search params', async () => {
|
||||
catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] });
|
||||
tokenIssuer.issueToken.mockResolvedValue('my-token');
|
||||
const client = new CatalogIdentityClient({
|
||||
catalogApi: catalogApi,
|
||||
tokenIssuer: tokenIssuer,
|
||||
catalogApi,
|
||||
tokenIssuer,
|
||||
});
|
||||
|
||||
await client.findUser({ annotations: { key: 'value' } });
|
||||
@@ -62,4 +66,93 @@ describe('CatalogIdentityClient', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolveCatalogMemberClaims resolves membership', async () => {
|
||||
const mockUsers: Array<UserEntityV1alpha1> = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'inigom',
|
||||
},
|
||||
spec: {
|
||||
memberOf: ['team-a'],
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: {
|
||||
kind: 'Group',
|
||||
namespace: 'default',
|
||||
name: 'team-a',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'mpatinkin',
|
||||
namespace: 'reality',
|
||||
},
|
||||
spec: {
|
||||
memberOf: ['screen-actors-guild'],
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: {
|
||||
kind: 'Group',
|
||||
namespace: 'reality',
|
||||
name: 'screen-actors-guild',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
catalogApi.getEntities.mockResolvedValueOnce({ items: mockUsers });
|
||||
|
||||
const client = new CatalogIdentityClient({
|
||||
catalogApi,
|
||||
tokenIssuer,
|
||||
});
|
||||
|
||||
const claims = await client.resolveCatalogMemberClaims('inigom', [
|
||||
'User:default/imontoya',
|
||||
'User:reality/mpatinkin',
|
||||
]);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: 'user',
|
||||
'metadata.namespace': 'default',
|
||||
'metadata.name': 'inigom',
|
||||
},
|
||||
{
|
||||
kind: 'user',
|
||||
'metadata.namespace': 'default',
|
||||
'metadata.name': 'imontoya',
|
||||
},
|
||||
{
|
||||
kind: 'user',
|
||||
'metadata.namespace': 'reality',
|
||||
'metadata.name': 'mpatinkin',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(claims).toMatchObject({
|
||||
claims: {
|
||||
sub: 'inigom',
|
||||
ent: [
|
||||
'user:default/imontoya',
|
||||
'user:reality/mpatinkin',
|
||||
'group:default/team-a',
|
||||
'group:reality/screen-actors-guild',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import {
|
||||
EntityName,
|
||||
parseEntityRef,
|
||||
RELATION_MEMBER_OF,
|
||||
stringifyEntityRef,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { TokenIssuer, TokenParams } from '../../identity';
|
||||
|
||||
type UserQuery = {
|
||||
annotations: Record<string, string>;
|
||||
@@ -64,4 +71,79 @@ export class CatalogIdentityClient {
|
||||
|
||||
return items[0] as UserEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve additional entity claims from the catalog, using the passed-in subject and entity
|
||||
* claims. Designed to be used within a `signInResolver` where additional entity claims might be
|
||||
* provided, but group membership and transient group membership lean on imported catalog
|
||||
* relations.
|
||||
*
|
||||
* Returns a claim structure that can be passed directly to `issueToken`, with the same sub and a
|
||||
* superset of `ent` claims.
|
||||
*/
|
||||
async resolveCatalogMemberClaims(
|
||||
sub: string,
|
||||
ent: string[],
|
||||
logger?: Logger,
|
||||
): Promise<TokenParams> {
|
||||
const subRef: EntityName = parseEntityRef(sub, {
|
||||
defaultKind: 'user',
|
||||
defaultNamespace: 'default',
|
||||
});
|
||||
|
||||
let entityRefs: Array<EntityName> = [];
|
||||
if (ent) {
|
||||
entityRefs = ent
|
||||
.map((ref: string) => {
|
||||
try {
|
||||
const parsedRef = parseEntityRef(ref.toLocaleLowerCase('en-US'));
|
||||
return parsedRef;
|
||||
} catch {
|
||||
logger?.debug(
|
||||
`Failed to parse entityRef from '${sub}' ent claim: ${ref}, ignoring`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((ref): ref is EntityName => ref !== null);
|
||||
}
|
||||
|
||||
const filter = [subRef].concat(entityRefs).map(ref => ({
|
||||
kind: ref.kind,
|
||||
'metadata.namespace': ref.namespace,
|
||||
'metadata.name': ref.name,
|
||||
}));
|
||||
const entities = await this.catalogApi
|
||||
.getEntities({ filter })
|
||||
.then(r => r.items);
|
||||
|
||||
if (entityRefs.length !== entities.length) {
|
||||
const foundEntityNames = entities.map(stringifyEntityRef);
|
||||
const missingEntityNames = entityRefs
|
||||
.map(stringifyEntityRef)
|
||||
.filter(s => !foundEntityNames.includes(s));
|
||||
logger?.debug(
|
||||
`Entities not found for '${sub}' claims: ${missingEntityNames.join()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const memberOf = entities.flatMap(
|
||||
e =>
|
||||
e!.relations
|
||||
?.filter(r => r.type === RELATION_MEMBER_OF)
|
||||
.map(r => r.target) ?? [],
|
||||
);
|
||||
|
||||
const newEnt = [...new Set(entityRefs.concat(memberOf))].map(
|
||||
stringifyEntityRef,
|
||||
);
|
||||
|
||||
logger?.debug(`Found claims for ${sub} in the catalog: ${newEnt.join()}`);
|
||||
return {
|
||||
claims: {
|
||||
sub,
|
||||
ent: newEnt,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user