Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions src/devops-dashboard-backstage-app/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,75 @@
# [Backstage](https://backstage.io)
# DevOps Dashboard Backstage app

This is your newly scaffolded Backstage App, Good Luck!
The [Backstage](https://backstage.io) host app for the
[Curity DevOps Dashboard plugin](plugins/devops-dashboard/README.md).

To start the app, run:
The app serves two purposes:

- The development harness for the plugin.
- The basis for the branded Backstage instance that Curity ships.

On top of a standard Backstage app it adds sign-in through the Curity
Identity Server (`packages/app/src/modules/auth`,
`packages/backend/src/modules/curityOidcAuthProvider.ts`), so the plugin can
obtain access tokens for the Curity admin API.

New to Backstage? Start with [Backstage in five minutes](docs/backstage-intro.md).

## Run

Prerequisites: a running Curity Identity Server with the
`devops_dashboard_backstage` OAuth client registered. Register it once with
the configuration shell (from the identity-server repo):

```sh
dist/bin/idsh <<'EOF'
configure
set environments environment services zones default-zone allowed-origins-for-cors [ http://localhost:3000 http://localhost:3001 ]
set environments environment admin-service http restconf oauth oauth-profile oauth-dev
set environments environment admin-service http restconf oauth client [ devops_dashboard_backstage ]
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage capabilities code
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage secret Password1
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage redirect-uris http://localhost:7007/api/auth/oidc/handler/frame
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope openid
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope profile
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope email
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope urn:se:curity:scopes:admin:api
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage audience [ devops_dashboard_backstage urn:se:curity:audiences:admin:api ]
set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage user-authentication allowed-authenticators testAuth-janedoe
commit comment "Register the Backstage dev client"
exit
exit
EOF
```

Notes on the values above:

- The `allowed-origins-for-cors` zone setting lets the browser call the
database-clients GraphQL API from the Backstage origin.
- The audience must contain **both** the client id (OIDC sign-in validates
the ID token's `aud` against it) and the admin API audience (required for
the RESTCONF allow-listing).

To show database clients, the profile also needs the feature enabled and the
GraphQL API authorized. Apply the two DevOps Dashboard enablement patches
(they configure the client data source, the GraphQL endpoint, the
authorization manager, and the groups claim the API requires):
`curity-web-ui/devops-dashboard/cypress/fixtures/enable-dashboard-patch-data.json`
and the `enable-database-clients` recipe in
`curity-web-ui/.claude/skills/enable-database-clients/`.

```sh
yarn install
git apply dev-secrets.patch # creates the gitignored app-config.local.yaml
yarn start
```

Frontend on `http://localhost:3000`, backend on `:7007`. Sign-in starts
automatically when the app opens.

## Tests

```sh
yarn test # unit tests
yarn test:e2e # Playwright, against the running app
```
293 changes: 293 additions & 0 deletions src/devops-dashboard-backstage-app/docs/backstage-intro.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions src/devops-dashboard-backstage-app/e2e-test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Page } from '@playwright/test';

/**
* The app signs in through Curity automatically on load (SignInPage `auto`),
* so the OAuth popup opens without any click. The dev server's test
* authenticator normally completes on its own and the popup closes itself;
* if the login app asks to pick a user, choose janedoe.
*/
export async function signInToCurity(page: Page): Promise<void> {
const popup = await page.waitForEvent('popup');
try {
await popup.waitForEvent('close', { timeout: 15_000 });
Comment on lines +9 to +12
} catch {
await popup
.getByText(/janedoe/i)
.first()
.click();
await popup.waitForEvent('close');
}
}
2 changes: 1 addition & 1 deletion src/devops-dashboard-backstage-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"jest": "~30.2.0",
"jsdom": "^27.1.0",
"node-gyp": "^10.0.0",
"prettier": "^2.3.2",
"prettier": "^3.6.2",
"typescript": "~5.8.0"
},
"resolutions": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
*/

import { test, expect } from '@playwright/test';
import { signInToCurity } from '../../../e2e-test-utils';

test('App should render the welcome page', async ({ page }) => {
test('App should render after signing in through Curity', async ({ page }) => {
await page.goto('/');

const enterButton = page.getByRole('button', { name: 'Enter' });
await expect(enterButton).toBeVisible();
await enterButton.click();
await signInToCurity(page);

const nav = page.getByRole('navigation', { name: 'sidebar nav' });
await expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.38",
"@backstage/plugin-user-settings": "^0.9.5",
"@backstage/ui": "^0.17.0",
"@internal/backstage-plugin-devops-dashboard": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"common": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import { CURITY_AUTH_PROVIDER_ID } from 'common';
* `curityAuthApiRef.getAccessToken(['urn:se:curity:scopes:admin:api'])`
*/
export const curityAuthApiRef = createApiRef<
OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
id: 'auth.curity',
});
Expand Down Expand Up @@ -55,7 +59,11 @@ export const curityAuthApi = ApiBlueprint.make({
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
defaultScopes: ['openid', 'profile', 'urn:se:curity:scopes:admin:api'],
defaultScopes: [
'openid',
'profile',
'urn:se:curity:scopes:admin:api',
],
}),
}),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { createBackendModule } from '@backstage/backend-plugin-api';
import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model';
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
authProvidersExtensionPoint,
createOAuthProviderFactory,
Expand Down
6 changes: 5 additions & 1 deletion src/devops-dashboard-backstage-app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export default defineConfig({
: [
{
command: 'yarn start app',
url: 'http://localhost:3000',
// Reuses an already-running dev server (honor a port override
// in app-config.local.yaml by setting PLAYWRIGHT_URL).
url: process.env.PLAYWRIGHT_URL ?? 'http://localhost:3000',
reuseExistingServer: true,
timeout: 120_000,
},
Expand All @@ -56,6 +58,8 @@ export default defineConfig({
baseURL:
process.env.PLAYWRIGHT_URL ??
(process.env.CI ? 'http://localhost:7007' : 'http://localhost:3000'),
// The sign-in popup goes through the dev Curity server's self-signed TLS.
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# DevOps Dashboard plugin

A Backstage plugin for the Curity DevOps Dashboard.

## What is the Curity DevOps Dashboard?

The [Curity Identity Server](https://curity.io) separates configuration from
runtime data. Server configuration belongs to administrators and lives in the
admin UI. Runtime data — such as OAuth clients stored in a database, sessions,
or tokens — changes while the server runs, and the people who need to see it
are usually operations and support teams, not administrators.

The DevOps Dashboard is the view for those teams. It shows the runtime data of
a running Curity Identity Server without giving access to server
configuration.

This plugin brings the DevOps Dashboard into [Backstage](https://backstage.io),
so teams can use it inside the developer portal they already work in. It adds
a **DevOps Dashboard** entry to the Backstage sidebar, with one tab per
section.

## Requirements

- A Backstage app built on the
[frontend system](https://backstage.io/docs/frontend-system/architecture/index).
- A running Curity Identity Server, reachable from the user's browser.
- Users must be able to authenticate against the Curity Identity Server, and
their access token must carry the admin API scope
(`urn:se:curity:scopes:admin:api`). The app can sign users in through
Curity directly, or keep its own sign-in and add Curity as an extra auth
provider. Users without the scope see an access-denied message.

## Install the plugin in your own Backstage app

Installation follows the standard Backstage steps:

1. Add the package to your app:

```sh
yarn --cwd packages/app add @internal/backstage-plugin-devops-dashboard
```

2. Let your app discover it. If your `app-config.yaml` enables package
discovery, you are done:

```yaml
app:
packages: all
```

Without discovery, register the plugin explicitly in your app's
`createApp` call:

```tsx
import devopsDashboardPlugin from '@internal/backstage-plugin-devops-dashboard';

const app = createApp({
features: [devopsDashboardPlugin],
});
```

3. Configure sign-in through Curity as described under
[Requirements](#requirements).

The **DevOps Dashboard** entry then appears in the sidebar, and the page is
available at `/devops-dashboard`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createDevApp } from '@backstage/frontend-dev-utils';

import plugin from '../src';

createDevApp({ features: [plugin] });
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from '@playwright/test';
import { signInToCurity } from '../../../e2e-test-utils';

test('renders the DevOps Dashboard page with the Database Clients section', async ({
page,
}) => {
await page.goto('/');
await signInToCurity(page);

const nav = page.getByRole('navigation', { name: 'sidebar nav' });
const dashboardLink = nav.getByRole('link', { name: 'DevOps Dashboard' });
await expect(dashboardLink).toBeVisible();
await dashboardLink.click();

const defaultSectionUrl = /\/devops-dashboard\/db-clients$/;
await expect(page).toHaveURL(defaultSectionUrl);
await expect(
page.getByRole('tab', { name: 'Database Clients' }),
).toBeVisible();
await expect(
page.getByRole('searchbox', { name: 'Search database clients' }),
).toBeVisible();
await expect(
page.getByRole('rowheader', { name: 'spa-client' }),
).toBeVisible();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@internal/backstage-plugin-devops-dashboard",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true,
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin",
"pluginId": "devops-dashboard"
},
"sideEffects": false,
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "^0.18.12",
"@backstage/frontend-plugin-api": "^0.17.3",
"@backstage/theme": "^0.7.3",
"@backstage/ui": "^0.17.0",
"@material-ui/icons": "^4.9.1",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.36.4",
"@backstage/frontend-defaults": "^0.5.4",
"@backstage/frontend-dev-utils": "^0.1.4",
"@backstage/frontend-test-utils": "^0.6.2",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",
"msw": "^1.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"files": [
"dist"
]
}
Loading