diff --git a/packages/backend/package.json b/packages/backend/package.json index b3a2fcb60b..f76a40a8f0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -38,6 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-backend": "workspace:^", "@backstage/plugin-azure-sites-backend": "workspace:^", + "@backstage/plugin-azure-sites-common": "workspace:^", "@backstage/plugin-badges-backend": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bade028597..323fc27005 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -41,6 +41,7 @@ import healthcheck from './plugins/healthcheck'; import { metricsHandler, metricsInit } from './metrics'; import auth from './plugins/auth'; import azureDevOps from './plugins/azure-devops'; +import azureSites from './plugins/azure-sites'; import catalog from './plugins/catalog'; import codeCoverage from './plugins/codecoverage'; import entityFeedback from './plugins/entityFeedback'; @@ -145,6 +146,7 @@ async function main() { const createEnv = makeCreateEnv(config); + const azureSitesEnv = useHotMemoize(module, () => createEnv('azure-sites')); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); const codeCoverageEnv = useHotMemoize(module, () => @@ -189,6 +191,7 @@ async function main() { apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv)); + apiRouter.use('/azure-sites', await azureSites(azureSitesEnv)); apiRouter.use('/search', await search(searchEnv)); apiRouter.use('/techdocs', await techdocs(techdocsEnv)); apiRouter.use('/todo', await todo(todoEnv)); diff --git a/packages/backend/src/plugins/azure-sites.ts b/packages/backend/src/plugins/azure-sites.ts new file mode 100644 index 0000000000..7e3f53092b --- /dev/null +++ b/packages/backend/src/plugins/azure-sites.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 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 { + createRouter, + AzureSitesApi, +} from '@backstage/plugin-azure-sites-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + ...env, + azureSitesApi: AzureSitesApi.fromConfig(env.config), + }); +} diff --git a/packages/backend/src/plugins/permission.ts b/packages/backend/src/plugins/permission.ts index 7192a1ddec..8d8116e627 100644 --- a/packages/backend/src/plugins/permission.ts +++ b/packages/backend/src/plugins/permission.ts @@ -19,6 +19,7 @@ import { createRouter } from '@backstage/plugin-permission-backend'; import { AuthorizeResult, PolicyDecision, + isPermission, } from '@backstage/plugin-permission-common'; import { PermissionPolicy, @@ -30,6 +31,11 @@ import { } from '@backstage/plugin-playlist-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; +import { azureSitesActionPermission } from '@backstage/plugin-azure-sites-common'; +import { + catalogConditions, + createCatalogConditionalDecision, +} from '@backstage/plugin-catalog-backend/alpha'; class ExamplePermissionPolicy implements PermissionPolicy { private playlistPermissionPolicy = new DefaultPlaylistPermissionPolicy(); @@ -42,6 +48,15 @@ class ExamplePermissionPolicy implements PermissionPolicy { return this.playlistPermissionPolicy.handle(request, user); } + if (isPermission(request.permission, azureSitesActionPermission)) { + return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner({ + claims: user?.identity.ownershipEntityRefs ?? [], + }), + ); + } + return { result: AuthorizeResult.ALLOW, }; diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 23152f9760..b5f6260a0e 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -2,7 +2,7 @@ Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries. -_Inspired by [roadie.io AWS Lambda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_ +_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_ ## Setup @@ -36,11 +36,11 @@ Here's how to get the backend plugin up and running: 1. First we need to add the `@backstage/plugin-azure-sites-backend` package to your backend: ```sh - # From your Backstage root directory + # From the Backstage root directory yarn add --cwd packages/backend @backstage/plugin-azure-sites-backend ``` -2. Then we will create a new file named `packages/backend/src/plugins/azure.ts`, and add the following to it: +2. Then we will create a new file named `packages/backend/src/plugins/azure-sites.ts`, and add the following to it: ```ts import { @@ -56,6 +56,7 @@ Here's how to get the backend plugin up and running: return await createRouter({ logger: env.logger, azureSitesApi: AzureSitesApi.fromConfig(env.config), + permissions: env.permissions, }); } ``` @@ -63,9 +64,9 @@ Here's how to get the backend plugin up and running: 3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: ```ts - import azure from './plugins/azure'; + import azureSites from './plugins/azure-sites'; - // Removed for clarity... + // Removed for clairty... async function main() { // ... @@ -76,10 +77,34 @@ Here's how to get the backend plugin up and running: // ... // Insert this line under the other lines that add their routers to apiRouter in the same way - apiRouter.use('/azure-sites', await azure(azureSitesEnv)); + apiRouter.use('/azure-sites', await azureSites(azureSitesEnv)); } ``` -4. Now run `yarn start-backend` from the repo root. +4. Enable permissions and that the below is just an example policy that forbids anyone but the owner of the catalog entity to trigger actions towards a site tied to an entity, edit your `packages/backend/src/plugins/permission.ts` -5. Finally, open `http://localhost:7007/api/azure-sites/health` in a browser, it should return `{"status":"ok"}`. + ```diff + // packages/backend/src/plugins/permission.ts + + import { azureSitesActionPermission } from '@backstage/plugin-azure-sites-common'; + ... + class TestPermissionPolicy implements PermissionPolicy { + - async handle(): Promise { + + async handle(request: PolicyQuery, user?: BackstageIdentityResponse): Promise { + if (isPermission(request.permission, azureSitesActionPermission)) { + return createCatalogConditionalDecision( + request.permission, + catalogConditions.isEntityOwner({ + claims: user?.identity.ownershipEntityRefs ?? [], + }), + ); + } + ... + return { + result: AuthorizeResult.ALLOW, + }; + } + ``` + +5. Now run `yarn start-backend` from the repo root. + +6. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`. diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index c4f689129d..4426798ca2 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -36,8 +36,13 @@ "@azure/arm-resourcegraph": "^4.2.1", "@azure/identity": "^4.0.0", "@backstage/backend-common": "workspace:^", + "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-sites-common": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", diff --git a/plugins/azure-sites-backend/src/api/AzureSitesApi.ts b/plugins/azure-sites-backend/src/api/AzureSitesApi.ts index 73e92ea6a0..b8660c5989 100644 --- a/plugins/azure-sites-backend/src/api/AzureSitesApi.ts +++ b/plugins/azure-sites-backend/src/api/AzureSitesApi.ts @@ -95,4 +95,15 @@ export class AzureSitesApi { } return { items: items }; } + + async validateSite(annotationName: string, siteName: string) { + const azureSites = await this.list({ + name: annotationName, + }); + for (const site of azureSites.items) { + if (site.name === siteName) return true; + } + + return false; + } } diff --git a/plugins/azure-sites-backend/src/service/router.ts b/plugins/azure-sites-backend/src/service/router.ts index bea9a15396..c7661239e5 100644 --- a/plugins/azure-sites-backend/src/service/router.ts +++ b/plugins/azure-sites-backend/src/service/router.ts @@ -19,19 +19,32 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { NotAllowedError } from '@backstage/errors'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { + PermissionEvaluator, + AuthorizeResult, +} from '@backstage/plugin-permission-common'; +import { + azureSitesActionPermission, + AZURE_WEB_SITE_NAME_ANNOTATION, +} from '@backstage/plugin-azure-sites-common'; + import { AzureSitesApi } from '../api'; /** @public */ export interface RouterOptions { logger: Logger; azureSitesApi: AzureSitesApi; + permissions: PermissionEvaluator; } /** @public */ export async function createRouter( options: RouterOptions, ): Promise { - const { logger, azureSitesApi } = options; + const { logger, azureSitesApi, permissions } = options; const router = Router(); router.use(express.json()); @@ -52,7 +65,33 @@ export async function createRouter( '/:subscription/:resourceGroup/:name/start', async (request, response) => { const { subscription, resourceGroup, name } = request.params; - console.log('starting...'); + const token = getBearerTokenFromAuthorizationHeader( + request.header('authorization'), + ); + const entity = request.body.entity; + const resourceRef = stringifyEntityRef(entity); + + const annotationName = + entity.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION]; + if (await azureSitesApi.validateSite(annotationName, name)) { + throw new NotAllowedError(); + } + + const decision = ( + await permissions.authorize( + [{ permission: azureSitesActionPermission, resourceRef }], + { + token, + }, + ) + )[0]; + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + + logger.info( + `entity ${entity.metadata.name} - azure site "${name}" is starting...`, + ); response.json( await azureSitesApi.start({ subscription, @@ -66,7 +105,33 @@ export async function createRouter( '/:subscription/:resourceGroup/:name/stop', async (request, response) => { const { subscription, resourceGroup, name } = request.params; - console.log('stopping...'); + const token = getBearerTokenFromAuthorizationHeader( + request.header('authorization'), + ); + const entity = request.body.entity; + const resourceRef = stringifyEntityRef(entity); + + const annotationName = + entity.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION]; + if (await azureSitesApi.validateSite(annotationName, name)) { + throw new NotAllowedError(); + } + + const decision = ( + await permissions.authorize( + [{ permission: azureSitesActionPermission, resourceRef }], + { + token, + }, + ) + )[0]; + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError('Unauthorized'); + } + + logger.info( + `entity ${entity.metadata.name} - azure site "${name}" is stopping...`, + ); response.json( await azureSitesApi.stop({ subscription, diff --git a/plugins/azure-sites-backend/src/service/standaloneServer.ts b/plugins/azure-sites-backend/src/service/standaloneServer.ts index 1998bee023..8c64fa7b1b 100644 --- a/plugins/azure-sites-backend/src/service/standaloneServer.ts +++ b/plugins/azure-sites-backend/src/service/standaloneServer.ts @@ -17,11 +17,14 @@ import { createServiceBuilder, loadBackendConfig, + ServerTokenManager, + SingleHostDiscovery, } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { AzureSitesApi } from '../api'; import { createRouter } from './router'; +import { ServerPermissionClient } from '@backstage/plugin-permission-node'; export interface ServerOptions { port: number; @@ -34,9 +37,18 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'azure-backend' }); const config = await loadBackendConfig({ logger, argv: process.argv }); + const discovery = SingleHostDiscovery.fromConfig(config); + const tokenManager = ServerTokenManager.fromConfig(config, { + logger, + }); + const permissions = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); logger.debug('Starting application server...'); const router = await createRouter({ logger, + permissions, azureSitesApi: AzureSitesApi.fromConfig(config), }); diff --git a/plugins/azure-sites-common/package.json b/plugins/azure-sites-common/package.json index 05815edd02..0365daa844 100644 --- a/plugins/azure-sites-common/package.json +++ b/plugins/azure-sites-common/package.json @@ -32,6 +32,11 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^" + }, "devDependencies": { "@backstage/cli": "workspace:^" }, diff --git a/plugins/azure-sites-common/src/index.ts b/plugins/azure-sites-common/src/index.ts index db229eae34..999a9c713c 100644 --- a/plugins/azure-sites-common/src/index.ts +++ b/plugins/azure-sites-common/src/index.ts @@ -15,3 +15,5 @@ */ export * from './types'; +export * from './utils'; +export * from './permissions'; diff --git a/plugins/azure-sites-common/src/permissions.ts b/plugins/azure-sites-common/src/permissions.ts new file mode 100644 index 0000000000..03b50f3edc --- /dev/null +++ b/plugins/azure-sites-common/src/permissions.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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 { createPermission } from '@backstage/plugin-permission-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; + +/** + * @public + */ +export const azureSitesActionPermission = createPermission({ + name: 'azure.sites.update', + attributes: { action: 'update' }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}); + +/** + * @public + */ +export const azureSitesPermissions = [azureSitesActionPermission]; diff --git a/plugins/azure-sites-common/src/types.ts b/plugins/azure-sites-common/src/types.ts index 7c8be7ce24..7bab33cef5 100644 --- a/plugins/azure-sites-common/src/types.ts +++ b/plugins/azure-sites-common/src/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; + /** @public */ export type AzureSite = { href: string; @@ -45,4 +47,5 @@ export type AzureSiteStartStopRequest = { subscription: string; resourceGroup: string; name: string; + entity?: Entity; }; diff --git a/plugins/azure-sites-common/src/utils.ts b/plugins/azure-sites-common/src/utils.ts new file mode 100644 index 0000000000..017f38a380 --- /dev/null +++ b/plugins/azure-sites-common/src/utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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. + */ + +/** + * @public + */ +export const AZURE_WEB_SITE_NAME_ANNOTATION = 'azure.com/microsoft-web-sites'; diff --git a/plugins/azure-sites/README.md b/plugins/azure-sites/README.md index 57bf84ccc5..a19b314b77 100644 --- a/plugins/azure-sites/README.md +++ b/plugins/azure-sites/README.md @@ -14,7 +14,7 @@ The following sections will help you get the Azure plugin setup and running ### Azure Sites Backend -You need to set up the Azure Sites Backend plugin before you move forward with any of these steps if you haven't already. +You need to set up the [Azure Sites Backend plugin](https://github.com/backstage/backstage/tree/master/plugins/azure-sites-backend) before you move forward with any of these steps if you haven't already. ### Entity Annotation diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index a4337348f3..c9777e73da 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -38,6 +38,9 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-azure-sites-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", + "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts index b66a3cbb3f..8c19f4a93d 100644 --- a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts +++ b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts @@ -39,12 +39,16 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { request.subscription }/${request.resourceGroup}/${request.name}/stop`; const { token: accessToken } = await this.identityApi.getCredentials(); + const entity = request.entity; await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }, + body: JSON.stringify({ + entity, + }), }); } async start(request: AzureSiteStartStopRequest): Promise { @@ -52,12 +56,16 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { request.subscription }/${request.resourceGroup}/${request.name}/start`; const { token: accessToken } = await this.identityApi.getCredentials(); + const entity = request.entity; await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }, + body: JSON.stringify({ + entity, + }), }); } diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx index dbed6cdbd6..71e5639da2 100644 --- a/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx +++ b/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx @@ -17,11 +17,9 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; import { useSites } from '../../hooks/useSites'; -import { - AZURE_WEB_SITE_NAME_ANNOTATION, - useServiceEntityAnnotations, -} from '../../hooks/useServiceEntityAnnotations'; import { ErrorBoundary, ResponseErrorPanel } from '@backstage/core-components'; +import { useServiceEntityAnnotations } from '../../hooks/useServiceEntityAnnotations'; +import { AZURE_WEB_SITE_NAME_ANNOTATION } from '@backstage/plugin-azure-sites-common'; import { useEntity, MissingAnnotationEmptyState, diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx index 5d1a5ee73b..f161da5ef0 100644 --- a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx +++ b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx @@ -21,6 +21,11 @@ import { errorApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { + PermissionApi, + permissionApiRef, +} from '@backstage/plugin-permission-react'; import { rest } from 'msw'; import { renderInTestApp, @@ -41,15 +46,33 @@ const identityApiMock = (getCredentials: any) => ({ }); const azureSitesApiMock = {}; +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntity: () => { + return { loading: false, entity: undefined }; + }, +})); + +jest.mock('@backstage/plugin-catalog-react/alpha', () => ({ + useEntityPermission: () => { + return { loading: false, allowed: true }; + }, +})); + const config = { getString: (_: string) => 'https://test-url', }; +const mockAuthorize = jest + .fn() + .mockImplementation(async () => ({ result: AuthorizeResult.ALLOW })); +const permissionApi: Partial = { authorize: mockAuthorize }; + const apis: [AnyApiRef, Partial][] = [ [errorApiRef, errorApiMock], [configApiRef, config], [azureSiteApiRef, azureSitesApiMock], [identityApiRef, identityApiMock], + [permissionApiRef, permissionApi], ]; describe('AzureSitesOverviewWidget', () => { diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx index c7f8befedb..2095dc5391 100644 --- a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx +++ b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx @@ -27,7 +27,10 @@ import { Tooltip, } from '@material-ui/core'; import { default as MuiAlert } from '@material-ui/lab/Alert'; -import { AzureSite } from '@backstage/plugin-azure-sites-common'; +import { + AzureSite, + azureSitesActionPermission, +} from '@backstage/plugin-azure-sites-common'; import { Table, TableColumn, Link } from '@backstage/core-components'; import { useTheme } from '@material-ui/core/styles'; import FlashOnIcon from '@material-ui/icons/FlashOn'; @@ -40,6 +43,8 @@ import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import { DateTime } from 'luxon'; import { useApi } from '@backstage/core-plugin-api'; import { azureSiteApiRef } from '../../api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; type States = 'Waiting' | 'Running' | 'Paused' | 'Failed' | 'Stopped'; type Kinds = 'app' | 'functionapp'; @@ -117,6 +122,7 @@ const ActionButtons = ({ onMenuItemClick: Dispatch>; }) => { const azureApi = useApi(azureSiteApiRef); + const { entity } = useEntity(); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); @@ -132,6 +138,7 @@ const ActionButtons = ({ name: value.name, resourceGroup: value.resourceGroup, subscription: value.subscription, + entity: entity, }); onMenuItemClick('Starting, this may take some time...'); handleClose(); @@ -141,11 +148,15 @@ const ActionButtons = ({ name: value.name, resourceGroup: value.resourceGroup, subscription: value.subscription, + entity: entity, }); onMenuItemClick('Stopping, this may take some time...'); handleClose(); }; + const { loading: loadingPermission, allowed: canDoAction } = + useEntityPermission(azureSitesActionPermission); + return (
- {value.state !== 'Running' && ( - + {value.state !== 'Running' && !loadingPermission && ( +  Start )} - {value.state !== 'Stopped' && ( - + {value.state !== 'Stopped' && !loadingPermission && ( +  Stop diff --git a/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts b/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts index 08070dc8e4..1695749640 100644 --- a/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts +++ b/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts @@ -15,8 +15,8 @@ */ import { Entity } from '@backstage/catalog-model'; +import { AZURE_WEB_SITE_NAME_ANNOTATION } from '@backstage/plugin-azure-sites-common'; -export const AZURE_WEB_SITE_NAME_ANNOTATION = 'azure.com/microsoft-web-sites'; export const useServiceEntityAnnotations = (entity: Entity) => { const webSiteName = entity?.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION] ?? ''; diff --git a/yarn.lock b/yarn.lock index 3324e9418a..5ed31502a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4969,9 +4969,14 @@ __metadata: "@azure/arm-resourcegraph": ^4.2.1 "@azure/identity": ^4.0.0 "@backstage/backend-common": "workspace:^" + "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-sites-common": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 express: ^4.17.1 @@ -4986,7 +4991,10 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-azure-sites-common@workspace:plugins/azure-sites-common" dependencies: + "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" languageName: unknown linkType: soft @@ -5001,6 +5009,8 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/plugin-azure-sites-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1