Merge pull request #20717 from deepan10/azure-sites-auth

Azure sites - Added Permission to do start and stop
This commit is contained in:
Ben Lambert
2024-01-24 11:01:19 +01:00
committed by GitHub
27 changed files with 460 additions and 43 deletions
+32
View File
@@ -0,0 +1,32 @@
---
'@backstage/plugin-azure-sites-backend': patch
'@backstage/plugin-azure-sites-common': patch
'@backstage/plugin-azure-sites': patch
---
Azure Sites `start` and `stop` action is now protected with the Permissions framework.
The below example describes an action that forbids anyone but the owner of the catalog entity to trigger actions towards a site tied to an entity.
```typescript
// packages/backend/src/plugins/permission.ts
import { azureSitesActionPermission } from '@backstage/plugin-azure-sites-common';
...
class TestPermissionPolicy implements PermissionPolicy {
async handle(request: PolicyQuery, user?: BackstageIdentityResponse): Promise<PolicyDecision> {
if (isPermission(request.permission, azureSitesActionPermission)) {
return createCatalogConditionalDecision(
request.permission,
catalogConditions.isEntityOwner({
claims: user?.identity.ownershipEntityRefs ?? [],
}),
);
}
...
return {
result: AuthorizeResult.ALLOW,
};
}
...
}
```
+31
View File
@@ -0,0 +1,31 @@
---
'@backstage/plugin-azure-sites-backend': minor
---
**BREAKING**: `catalogApi` and `permissionsApi` are now a requirement to be passed through to the `createRouter` function.
You can fix the typescript issues by passing through the required dependencies like the below `diff` shows:
```diff
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<Router> {
+ const catalogClient = new CatalogClient({
+ discoveryApi: env.discovery,
+ });
return await createRouter({
logger: env.logger,
azureSitesApi: AzureSitesApi.fromConfig(env.config),
+ catalogApi: catalogClient,
+ permissionsApi: env.permissions,
});
}
```
+1
View File
@@ -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:^",
+3
View File
@@ -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));
@@ -0,0 +1,34 @@
/*
* 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';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
return await createRouter({
logger: env.logger,
permissions: env.permissions,
azureSitesApi: AzureSitesApi.fromConfig(env.config),
catalogApi,
});
}
+31 -6
View File
@@ -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,7 +64,7 @@ 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...
@@ -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<PolicyDecision> {
+ async handle(request: PolicyQuery, user?: BackstageIdentityResponse): Promise<PolicyDecision> {
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"}`.
@@ -6,9 +6,11 @@
import { AzureSiteListRequest } from '@backstage/plugin-azure-sites-common';
import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common';
import { AzureSiteStartStopRequest } from '@backstage/plugin-azure-sites-common';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import express from 'express';
import { Logger } from 'winston';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
// @public (undocumented)
export class AzureSitesApi {
@@ -21,6 +23,8 @@ export class AzureSitesApi {
start(request: AzureSiteStartStopRequest): Promise<void>;
// (undocumented)
stop(request: AzureSiteStartStopRequest): Promise<void>;
// (undocumented)
validateSite(annotationName: string, siteName: string): Promise<boolean>;
}
// @public (undocumented)
@@ -54,7 +58,11 @@ export interface RouterOptions {
// (undocumented)
azureSitesApi: AzureSitesApi;
// (undocumented)
catalogApi: CatalogApi;
// (undocumented)
logger: Logger;
// (undocumented)
permissions?: PermissionEvaluator;
}
// (No @packageDocumentation comment for this package)
+6
View File
@@ -36,8 +36,14 @@
"@azure/arm-resourcegraph": "^4.2.1",
"@azure/identity": "^4.0.0",
"@backstage/backend-common": "workspace:^",
"@backstage/catalog-client": "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",
@@ -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;
}
}
+117 -15
View File
@@ -19,19 +19,33 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { InputError, NotAllowedError, NotFoundError } 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 { CatalogApi } from '@backstage/catalog-client';
import { AzureSitesApi } from '../api';
/** @public */
export interface RouterOptions {
logger: Logger;
azureSitesApi: AzureSitesApi;
catalogApi: CatalogApi;
permissions?: PermissionEvaluator;
}
/** @public */
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, azureSitesApi } = options;
const { logger, azureSitesApi, permissions, catalogApi } = options;
const router = Router();
router.use(express.json());
@@ -52,28 +66,116 @@ export async function createRouter(
'/:subscription/:resourceGroup/:name/start',
async (request, response) => {
const { subscription, resourceGroup, name } = request.params;
console.log('starting...');
response.json(
await azureSitesApi.start({
subscription,
resourceGroup,
name,
}),
const token = getBearerTokenFromAuthorizationHeader(
request.header('authorization'),
);
const entityRef = request.body.entityRef;
if (typeof entityRef !== 'string') {
throw new InputError('Invalid entityRef, not a string');
}
const entity = await catalogApi.getEntityByRef(entityRef, { token });
if (entity) {
const annotationName =
entity.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION];
if (
annotationName &&
!(await azureSitesApi.validateSite(annotationName, name))
) {
throw new NotFoundError();
}
const decision = permissions
? (
await permissions.authorize(
[
{
permission: azureSitesActionPermission,
resourceRef: entityRef,
},
],
{
token,
},
)
)[0]
: undefined;
if (decision && 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,
resourceGroup,
name,
}),
);
} else {
throw new NotFoundError();
}
},
);
router.post(
'/:subscription/:resourceGroup/:name/stop',
async (request, response) => {
const { subscription, resourceGroup, name } = request.params;
console.log('stopping...');
response.json(
await azureSitesApi.stop({
subscription,
resourceGroup,
name,
}),
const token = getBearerTokenFromAuthorizationHeader(
request.header('authorization'),
);
const entityRef = request.body.entityRef;
if (typeof entityRef !== 'string') {
throw new InputError('Invalid entityRef, not a string');
}
const entity = await catalogApi.getEntityByRef(entityRef, { token });
if (entity) {
const annotationName =
entity.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION];
if (
annotationName &&
!(await azureSitesApi.validateSite(annotationName, name))
) {
throw new NotFoundError('annotation mismatched!');
}
const decision = permissions
? (
await permissions.authorize(
[
{
permission: azureSitesActionPermission,
resourceRef: entityRef,
},
],
{
token,
},
)
)[0]
: undefined;
if (decision && 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,
resourceGroup,
name,
}),
);
} else {
throw new NotFoundError();
}
},
);
router.use(errorHandler());
@@ -17,11 +17,15 @@
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';
import { CatalogClient } from '@backstage/catalog-client';
export interface ServerOptions {
port: number;
@@ -34,10 +38,21 @@ export async function startStandaloneServer(
): Promise<Server> {
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,
});
const catalogApi = new CatalogClient({ discoveryApi: discovery });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
permissions,
azureSitesApi: AzureSitesApi.fromConfig(config),
catalogApi,
});
let service = createServiceBuilder(module)
+19
View File
@@ -3,6 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ResourcePermission } from '@backstage/plugin-permission-common';
// @public (undocumented)
export const AZURE_WEB_SITE_NAME_ANNOTATION = 'azure.com/microsoft-web-sites';
// @public (undocumented)
export type AzureSite = {
href: string;
@@ -19,6 +24,14 @@ export type AzureSite = {
tags: {};
};
// @public (undocumented)
export type AzureSiteBackendRequest = {
subscription: string;
resourceGroup: string;
name: string;
entityRef: string;
};
// @public (undocumented)
export type AzureSiteListRequest = {
name: string;
@@ -29,6 +42,12 @@ export type AzureSiteListResponse = {
items: AzureSite[];
};
// @public (undocumented)
export const azureSitesActionPermission: ResourcePermission<'catalog-entity'>;
// @public (undocumented)
export const azureSitesPermissions: ResourcePermission<'catalog-entity'>[];
// @public (undocumented)
export type AzureSiteStartStopRequest = {
subscription: string;
+5
View File
@@ -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:^"
},
+2
View File
@@ -15,3 +15,5 @@
*/
export * from './types';
export * from './utils';
export * from './permissions';
@@ -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];
+8
View File
@@ -46,3 +46,11 @@ export type AzureSiteStartStopRequest = {
resourceGroup: string;
name: string;
};
/** @public */
export type AzureSiteBackendRequest = {
subscription: string;
resourceGroup: string;
name: string;
entityRef: string;
};
+20
View File
@@ -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';
+1 -1
View File
@@ -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
+5 -5
View File
@@ -6,9 +6,9 @@
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { AzureSiteBackendRequest } from '@backstage/plugin-azure-sites-common';
import { AzureSiteListRequest } from '@backstage/plugin-azure-sites-common';
import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common';
import { AzureSiteStartStopRequest } from '@backstage/plugin-azure-sites-common';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
@@ -23,8 +23,8 @@ export const azureSiteApiRef: ApiRef<AzureSitesApi>;
// @public (undocumented)
export type AzureSitesApi = {
list: (request: AzureSiteListRequest) => Promise<AzureSiteListResponse>;
start: (request: AzureSiteStartStopRequest) => Promise<void>;
stop: (request: AzureSiteStartStopRequest) => Promise<void>;
start: (request: AzureSiteBackendRequest) => Promise<void>;
stop: (request: AzureSiteBackendRequest) => Promise<void>;
};
// @public (undocumented)
@@ -36,9 +36,9 @@ export class AzureSitesApiBackendClient implements AzureSitesApi {
// (undocumented)
list(request: AzureSiteListRequest): Promise<AzureSiteListResponse>;
// (undocumented)
start(request: AzureSiteStartStopRequest): Promise<void>;
start(request: AzureSiteBackendRequest): Promise<void>;
// (undocumented)
stop(request: AzureSiteStartStopRequest): Promise<void>;
stop(request: AzureSiteBackendRequest): Promise<void>;
}
// @public (undocumented)
+3
View File
@@ -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",
+3 -3
View File
@@ -18,7 +18,7 @@ import { createApiRef } from '@backstage/core-plugin-api';
import {
AzureSiteListRequest,
AzureSiteListResponse,
AzureSiteStartStopRequest,
AzureSiteBackendRequest,
} from '@backstage/plugin-azure-sites-common';
/** @public */
@@ -29,6 +29,6 @@ export const azureSiteApiRef = createApiRef<AzureSitesApi>({
/** @public */
export type AzureSitesApi = {
list: (request: AzureSiteListRequest) => Promise<AzureSiteListResponse>;
start: (request: AzureSiteStartStopRequest) => Promise<void>;
stop: (request: AzureSiteStartStopRequest) => Promise<void>;
start: (request: AzureSiteBackendRequest) => Promise<void>;
stop: (request: AzureSiteBackendRequest) => Promise<void>;
};
@@ -18,7 +18,7 @@ import { AzureSitesApi } from './AzureSitesApi';
import {
AzureSiteListRequest,
AzureSiteListResponse,
AzureSiteStartStopRequest,
AzureSiteBackendRequest,
} from '@backstage/plugin-azure-sites-common';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
@@ -34,30 +34,38 @@ export class AzureSitesApiBackendClient implements AzureSitesApi {
this.identityApi = options.identityApi;
}
async stop(request: AzureSiteStartStopRequest): Promise<void> {
async stop(request: AzureSiteBackendRequest): Promise<void> {
const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/${
request.subscription
}/${request.resourceGroup}/${request.name}/stop`;
const { token: accessToken } = await this.identityApi.getCredentials();
const entityRef = request.entityRef;
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
body: JSON.stringify({
entityRef,
}),
});
}
async start(request: AzureSiteStartStopRequest): Promise<void> {
async start(request: AzureSiteBackendRequest): Promise<void> {
const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/${
request.subscription
}/${request.resourceGroup}/${request.name}/start`;
const { token: accessToken } = await this.identityApi.getCredentials();
const entityRef = request.entityRef;
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
body: JSON.stringify({
entityRef,
}),
});
}
@@ -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,
@@ -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,39 @@ 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 };
},
}));
jest.mock('@backstage/catalog-model', () => ({
stringifyEntityRef: () => {
return {};
},
}));
const config = {
getString: (_: string) => 'https://test-url',
};
const mockAuthorize = jest
.fn()
.mockImplementation(async () => ({ result: AuthorizeResult.ALLOW }));
const permissionApi: Partial<PermissionApi> = { authorize: mockAuthorize };
const apis: [AnyApiRef, Partial<unknown>][] = [
[errorApiRef, errorApiMock],
[configApiRef, config],
[azureSiteApiRef, azureSitesApiMock],
[identityApiRef, identityApiMock],
[permissionApiRef, permissionApi],
];
describe('AzureSitesOverviewWidget', () => {
@@ -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,9 @@ 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';
import { stringifyEntityRef } from '@backstage/catalog-model';
type States = 'Waiting' | 'Running' | 'Paused' | 'Failed' | 'Stopped';
type Kinds = 'app' | 'functionapp';
@@ -117,6 +123,8 @@ const ActionButtons = ({
onMenuItemClick: Dispatch<React.SetStateAction<string | null>>;
}) => {
const azureApi = useApi(azureSiteApiRef);
const { entity } = useEntity();
const entityRef = stringifyEntityRef(entity);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
@@ -132,6 +140,7 @@ const ActionButtons = ({
name: value.name,
resourceGroup: value.resourceGroup,
subscription: value.subscription,
entityRef: entityRef,
});
onMenuItemClick('Starting, this may take some time...');
handleClose();
@@ -141,11 +150,15 @@ const ActionButtons = ({
name: value.name,
resourceGroup: value.resourceGroup,
subscription: value.subscription,
entityRef: entityRef,
});
onMenuItemClick('Stopping, this may take some time...');
handleClose();
};
const { loading: loadingPermission, allowed: canDoAction } =
useEntityPermission(azureSitesActionPermission);
return (
<div>
<IconButton
@@ -173,14 +186,14 @@ const ActionButtons = ({
},
}}
>
{value.state !== 'Running' && (
<MenuItem key="start" onClick={start}>
{value.state !== 'Running' && !loadingPermission && (
<MenuItem key="start" onClick={start} disabled={!canDoAction}>
<StartIcon />
&nbsp;Start
</MenuItem>
)}
{value.state !== 'Stopped' && (
<MenuItem key="stop" onClick={stop}>
{value.state !== 'Stopped' && !loadingPermission && (
<MenuItem key="stop" onClick={stop} disabled={!canDoAction}>
<StopIcon />
&nbsp;Stop
</MenuItem>
@@ -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] ?? '';
+13
View File
@@ -4969,9 +4969,15 @@ __metadata:
"@azure/arm-resourcegraph": ^4.2.1
"@azure/identity": ^4.0.0
"@backstage/backend-common": "workspace:^"
"@backstage/catalog-client": "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 +4992,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,7 +5010,10 @@ __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:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@material-ui/lab": 4.0.0-alpha.61
@@ -26723,6 +26735,7 @@ __metadata:
"@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:^"