Merge pull request #3495 from SDA-SE/feat/group-ownership

Derive the owned entities in the catalog from group memberships
This commit is contained in:
Fredrik Adelöw
2020-12-01 10:41:29 +01:00
committed by GitHub
8 changed files with 305 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Use the OWNED_BY relation and compare it to the users MEMBER_OF relation. The user entity is searched by name, based on the userId of the identity.
@@ -15,7 +15,11 @@
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
Entity,
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import {
ApiProvider,
ApiRegistry,
@@ -46,6 +50,12 @@ describe('CatalogPage', () => {
owner: 'tools@example.com',
type: 'service',
},
relations: [
{
type: RELATION_OWNED_BY,
target: { kind: 'Group', name: 'tools', namespace: 'default' },
},
],
},
{
apiVersion: 'backstage.io/v1alpha1',
@@ -57,11 +67,34 @@ describe('CatalogPage', () => {
owner: 'not-tools@example.com',
type: 'service',
},
relations: [
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'not-tools',
namespace: 'default',
},
},
],
},
] as Entity[],
}),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
getEntityByName: async entityName => {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: { name: entityName.name },
relations: [
{
type: RELATION_MEMBER_OF,
target: { namespace: 'default', kind: 'Group', name: 'tools' },
},
],
};
},
};
const testProfile: Partial<ProfileInfo> = {
displayName: 'Display Name',
@@ -19,7 +19,6 @@ import {
Content,
ContentHeader,
errorApiRef,
identityApiRef,
SupportButton,
useApi,
} from '@backstage/core';
@@ -38,6 +37,8 @@ import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
import CatalogLayout from './CatalogLayout';
import { CatalogTabs, LabeledComponentType } from './CatalogTabs';
import { WelcomeBanner } from './WelcomeBanner';
import { useOwnUser } from '../useOwnUser';
import { isOwnerOf } from '../isOwnerOf';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -65,7 +66,6 @@ const CatalogPageContents = () => {
const catalogApi = useApi(catalogApiRef);
const errorApi = useApi(errorApiRef);
const { isStarredEntity } = useStarredEntities();
const userId = useApi(identityApiRef).getUserId();
const [selectedTab, setSelectedTab] = useState<string>();
const [selectedSidebarItem, setSelectedSidebarItem] = useState<string>();
const orgName = configApi.getOptionalString('organization.name') ?? 'Company';
@@ -112,6 +112,8 @@ const CatalogPageContents = () => {
[],
);
const { value: user } = useOwnUser();
const filterGroups = useMemo<ButtonGroup[]>(
() => [
{
@@ -121,7 +123,7 @@ const CatalogPageContents = () => {
id: 'owned',
label: 'Owned',
icon: SettingsIcon,
filterFn: entity => entity.spec?.owner === userId,
filterFn: entity => user !== undefined && isOwnerOf(user, entity),
},
{
id: 'starred',
@@ -142,7 +144,7 @@ const CatalogPageContents = () => {
],
},
],
[isStarredEntity, userId, orgName],
[isStarredEntity, orgName, user],
);
const showAddExampleEntities =
@@ -0,0 +1,66 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getEntityRelations } from './getEntityRelations';
import {
Entity,
RELATION_CHILD_OF,
RELATION_MEMBER_OF,
} from '@backstage/catalog-model';
describe('getEntityRelations', () => {
it('should handle undefined value', () => {
expect(getEntityRelations(undefined, RELATION_MEMBER_OF)).toEqual([]);
});
it('should extract correct relation', () => {
const entity = {
relations: [
{
type: RELATION_MEMBER_OF,
target: { kind: 'group', name: 'member' },
},
{
type: RELATION_CHILD_OF,
target: { kind: 'group', name: 'child' },
},
],
} as Entity;
expect(getEntityRelations(entity, RELATION_MEMBER_OF)).toEqual([
{ kind: 'group', name: 'member' },
]);
});
it('should filter relation by type', () => {
const entity = {
relations: [
{
type: RELATION_MEMBER_OF,
target: { kind: 'group', name: 'member' },
},
{
type: RELATION_MEMBER_OF,
target: { kind: 'user', name: 'child' },
},
],
} as Entity;
expect(
getEntityRelations(entity, RELATION_MEMBER_OF, { kind: 'Group' }),
).toEqual([{ kind: 'group', name: 'member' }]);
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity, EntityName } from '@backstage/catalog-model';
/**
* Get the related entity references.
*/
export function getEntityRelations(
entity: Entity | undefined,
relationType: string,
filter?: { kind: string },
): EntityName[] {
let entityNames =
entity?.relations
?.filter(r => r.type === relationType)
?.map(r => r.target) || [];
if (filter?.kind) {
entityNames = entityNames?.filter(
e => e.kind.toLowerCase() === filter.kind.toLowerCase(),
);
}
return entityNames;
}
@@ -0,0 +1,64 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Entity,
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { isOwnerOf } from './isOwnerOf';
describe('isOwnerOf', () => {
it('should be owned by user', () => {
const ownerEntity = {
kind: 'User',
metadata: { name: 'User', namespace: 'Default' },
} as Entity;
const ownedEntity = {
relations: [
{
type: RELATION_OWNED_BY,
target: { kind: 'user', namespace: 'default', name: 'user' },
},
],
} as Entity;
expect(isOwnerOf(ownerEntity, ownedEntity)).toBe(true);
});
it('should be owned by group', () => {
const ownerEntity = {
kind: 'User',
metadata: { name: 'user' },
relations: [
{
type: RELATION_MEMBER_OF,
target: { kind: 'group', namespace: 'default', name: 'group' },
},
],
} as Entity;
const ownedEntity = {
relations: [
{
type: RELATION_OWNED_BY,
target: { kind: 'group', namespace: 'default', name: 'group' },
},
],
} as Entity;
expect(isOwnerOf(ownerEntity, ownedEntity)).toBe(true);
});
});
@@ -0,0 +1,51 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Entity,
EntityName,
getEntityName,
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { getEntityRelations } from './getEntityRelations';
/**
* Get the related entity references.
*/
export function isOwnerOf(owner: Entity, owned: Entity) {
const possibleOwners: EntityName[] = [
...getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'group' }),
...(owner ? [getEntityName(owner)] : []),
];
const owners = getEntityRelations(owned, RELATION_OWNED_BY);
for (const owner of owners) {
if (
possibleOwners.find(
o =>
owner.kind.toLowerCase() === o.kind.toLowerCase() &&
owner.namespace.toLowerCase() === o.namespace.toLowerCase() &&
owner.name.toLowerCase() === o.name.toLowerCase(),
) !== undefined
) {
return true;
}
}
return false;
}
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UserEntity } from '@backstage/catalog-model';
import { useAsync } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsync';
import { identityApiRef, useApi } from '@backstage/core';
import { catalogApiRef } from '../plugin';
/**
* Get the group memberships of the logged-in user.
*/
export function useOwnUser(): AsyncState<UserEntity> {
const catalogApi = useApi(catalogApiRef);
const identityApi = useApi(identityApiRef);
// TODO: get the full entity (or at least the full entity name) from the identityApi
return useAsync(
() =>
catalogApi.getEntityByName({
kind: 'User',
namespace: 'default',
name: identityApi.getUserId(),
}) as Promise<UserEntity>,
[catalogApi, identityApi],
);
}