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:
@@ -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],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user