Disable unregister entity button in catalog if unauthorized

Signed-off-by: Joon Park <joonp@spotify.com>
This commit is contained in:
Joon Park
2022-01-17 17:59:59 +00:00
parent 6680853e0c
commit c54c0d9d10
20 changed files with 270 additions and 1 deletions
+7
View File
@@ -6,6 +6,7 @@
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { AsyncPermissionResult } from '@backstage/plugin-permission-react';
import { AsyncState } from 'react-use/lib/useAsync';
import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
import { CatalogApi } from '@backstage/catalog-client';
@@ -20,6 +21,7 @@ import { IdentityApi } from '@backstage/core-plugin-api';
import { LinkProps } from '@backstage/core-components';
import { Observable } from '@backstage/types';
import { Overrides } from '@material-ui/core/styles/overrides';
import { Permission } from '@backstage/plugin-permission-common';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
@@ -879,6 +881,11 @@ export function useEntityOwnership(): {
isOwnedEntity: (entity: Entity | EntityName) => boolean;
};
// @public
export function useEntityPermission(
permission: Permission,
): AsyncPermissionResult;
// Warning: (ae-forgotten-export) The symbol "EntityTypeReturn" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "useEntityTypeFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+3
View File
@@ -35,6 +35,8 @@
"@backstage/core-plugin-api": "^0.6.0-next.0",
"@backstage/errors": "^0.2.0",
"@backstage/integration": "^0.7.2-next.0",
"@backstage/plugin-permission-common": "^0.3.1",
"@backstage/plugin-permission-react": "^0.2.2",
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.1",
"@material-ui/core": "^4.12.2",
@@ -54,6 +56,7 @@
"devDependencies": {
"@backstage/cli": "^0.12.0-next.0",
"@backstage/core-app-api": "^0.5.0-next.0",
"@backstage/plugin-catalog-common": "^0.1.0",
"@backstage/test-utils": "^0.2.3-next.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+1
View File
@@ -43,3 +43,4 @@ export {
loadIdentityOwnerRefs,
} from './useEntityOwnership';
export { useOwnedEntities } from './useOwnedEntities';
export { useEntityPermission } from './useEntityPermission';
@@ -0,0 +1,64 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 React, { PropsWithChildren } from 'react';
import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common';
import { renderHook } from '@testing-library/react-hooks';
import { useEntityPermission } from './useEntityPermission';
import { MockPermissionApi, TestApiProvider } from '@backstage/test-utils';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from './useEntity';
const mockPermissionApi = new MockPermissionApi();
function createWrapper(entity?: Entity) {
return ({ children }: PropsWithChildren<{}>) => (
<TestApiProvider apis={[[permissionApiRef, mockPermissionApi]]}>
<EntityProvider entity={entity} children={children} />
</TestApiProvider>
);
}
describe('useEntityPermission', () => {
it('returns authorization result', async () => {
const { result, waitForValueToChange } = renderHook(
() => useEntityPermission(catalogEntityDeletePermission),
{
wrapper: createWrapper({
apiVersion: 'a',
kind: 'b',
metadata: { name: 'c' },
}),
},
);
await waitForValueToChange(() => result.current);
expect(result.current.allowed).toBe(true);
});
it('throws error if no entity is found', async () => {
const { waitForNextUpdate } = renderHook(
() => useEntityPermission(catalogEntityDeletePermission),
{
wrapper: createWrapper(),
},
);
await expect(() => waitForNextUpdate()).rejects.toThrowError();
});
});
@@ -0,0 +1,35 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { stringifyEntityRef } from '@backstage/catalog-model';
import { Permission } from '@backstage/plugin-permission-common';
import { usePermission } from '@backstage/plugin-permission-react';
import { useEntity } from './useEntity';
/**
* A thin wrapper around the
* {@link @backstage/plugin-permission-react#usePermission} hook which uses the
* current entity in context to make an authorization request for the given
* permission.
* @public
*/
export function useEntityPermission(permission: Permission) {
const { entity } = useEntity();
if (!entity) {
throw new Error('No entity in current context.');
}
return usePermission(permission, stringifyEntityRef(entity));
}
+1
View File
@@ -37,6 +37,7 @@
"@backstage/core-plugin-api": "^0.6.0-next.0",
"@backstage/errors": "^0.2.0",
"@backstage/integration-react": "^0.1.19-next.0",
"@backstage/plugin-catalog-common": "^0.1.0",
"@backstage/plugin-catalog-react": "^0.6.12-next.0",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
@@ -20,6 +20,11 @@ import { fireEvent, screen } from '@testing-library/react';
import * as React from 'react';
import { EntityContextMenu } from './EntityContextMenu';
jest.mock('@backstage/plugin-catalog-react', () => ({
...jest.requireActual('@backstage/plugin-catalog-react'),
useEntityPermission: () => ({ isAllowed: true }),
}));
describe('ComponentContextMenu', () => {
it('should call onUnregisterEntity on button click', async () => {
const mockCallback = jest.fn();
@@ -28,6 +28,8 @@ import Cancel from '@material-ui/icons/Cancel';
import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
import { IconComponent } from '@backstage/core-plugin-api';
import { useEntityPermission } from '@backstage/plugin-catalog-react';
import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common';
// TODO(freben): It should probably instead be the case that Header sets the theme text color to white inside itself unconditionally instead
const useStyles = makeStyles({
@@ -62,6 +64,9 @@ export const EntityContextMenu = ({
}: Props) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
const classes = useStyles();
const unregisterPermission = useEntityPermission(
catalogEntityDeletePermission,
);
const onOpen = (event: React.SyntheticEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
@@ -90,7 +95,9 @@ export const EntityContextMenu = ({
];
const disableUnregister =
UNSTABLE_contextMenuOptions?.disableUnregister ?? false;
(!unregisterPermission.allowed ||
UNSTABLE_contextMenuOptions?.disableUnregister) ??
false;
return (
<>
@@ -36,6 +36,11 @@ import React from 'react';
import { Route, Routes } from 'react-router';
import { EntityLayout } from './EntityLayout';
jest.mock('@backstage/plugin-catalog-react', () => ({
...jest.requireActual('@backstage/plugin-catalog-react'),
useEntityPermission: () => ({ isAllowed: true }),
}));
const mockEntity = {
kind: 'MyKind',
metadata: {