Skip to content
Open
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
11 changes: 10 additions & 1 deletion workspaces/x2a/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@ backend:
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
cors:
origin: http://localhost:3000
origin:
- http://localhost:3000
- http://localhost:6274
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
# This is for local development only, it is not recommended to use this in production
# The production database configuration is stored in app-config.production.yaml
database:
client: better-sqlite3
connection: ':memory:'
actions:
pluginSources:
- 'catalog'
- 'x2a'
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir

proxy:
Expand Down Expand Up @@ -72,6 +78,9 @@ integrations:
- host: bitbucket.org

auth:
experimentalDynamicClientRegistration:
# enable the feature
enabled: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be auth enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is what allows the oauth flow in MCP

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops sorry. I meant "Auto enabled". as in do we want this enabled by default

# see https://backstage.io/docs/auth/ to learn about auth providers
environment: development
providers:
Expand Down
3 changes: 2 additions & 1 deletion workspaces/x2a/examples/example-rbac-policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ g, user:development/guest, role:default/x2aUser
g, group:default/x2a-admin-group, role:default/x2aAdmin

g, user:default/elai-shalev, role:default/x2aAdmin
g, user:default/eloycoto, role:default/x2aAdmin

########################################################
# Catalog and Scaffolder templates permissions
Expand Down Expand Up @@ -47,4 +48,4 @@ p, role:default/x2aUser, scaffolder.template.parameter.read, read, allow
p, role:default/x2aUser, scaffolder.template.step.read, read, allow
p, role:default/x2aUser, scaffolder.task.create, create, allow
p, role:default/x2aUser, scaffolder.task.cancel, cancel, allow
p, role:default/x2aUser, scaffolder.task.read, read, allow
p, role:default/x2aUser, scaffolder.task.read, read, allow
10 changes: 10 additions & 0 deletions workspaces/x2a/examples/org.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,13 @@ spec:
displayName: Elai Shalev
email: foo@bar.com
memberOf: []
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: eloycoto
spec:
profile:
displayName: Eloy Coto
email: foo@bar.com
memberOf: []
2 changes: 2 additions & 0 deletions workspaces/x2a/packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
githubAuthApiRef,
gitlabAuthApiRef,
} from '@backstage/core-plugin-api';
import { ConsentPage } from './components/oauth2/ConsentPage';

const app = createApp({
apis,
Expand Down Expand Up @@ -152,6 +153,7 @@ const routes = (
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
<Route path="/notifications" element={<NotificationsPage />} />
<Route path="/oauth2/authorize/:sessionId" element={<ConsentPage />} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be difficult to distribute within RHDH. Easy with Backstage upstream deployments.

</FlatRoutes>
);

Expand Down
249 changes: 249 additions & 0 deletions workspaces/x2a/packages/app/src/components/oauth2/ConsentPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
* Copyright Red Hat, Inc.
*
* 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 { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
useApi,
configApiRef,
fetchApiRef,
discoveryApiRef,
} from '@backstage/core-plugin-api';
import {
Page,
Header,
Content,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import {
Card,
CardContent,
CardActions,
Button,
Typography,
Divider,
Box,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';

interface Session {
id: string;
clientName?: string;
clientId: string;
redirectUri: string;
scopes?: string[];
}

type ConsentState =
| { status: 'loading' }
| { status: 'error'; error: string }
| { status: 'loaded'; session: Session }
| { status: 'submitting'; session: Session; action: 'approve' | 'reject' }
| { status: 'completed'; action: 'approve' | 'reject' };

const useStyles = makeStyles(theme => ({
card: {
maxWidth: 600,
margin: `${theme.spacing(4)}px auto`,
},
appHeader: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(2),
marginBottom: theme.spacing(2),
},
callbackUrl: {
fontFamily: 'monospace',
fontSize: '0.85rem',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should be taken from theme

backgroundColor: theme.palette.background.default,
padding: theme.spacing(1),
borderRadius: theme.shape.borderRadius,
wordBreak: 'break-all',
marginTop: theme.spacing(1),
},
actions: {
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(1),
padding: theme.spacing(2),
},
}));

const getHeaderTitle = (state: ConsentState): string => {
if (state.status === 'completed' && state.action === 'approve') {
return 'Authorization Approved';
}
if (state.status === 'completed') {
return 'Authorization Denied';
}
return 'Authorization Request';
};

export const ConsentPage = () => {
const { sessionId } = useParams<{ sessionId: string }>();
const classes = useStyles();
const configApi = useApi(configApiRef);
const fetchApi = useApi(fetchApiRef);
const discoveryApi = useApi(discoveryApiRef);
const appTitle = configApi.getOptionalString('app.title') ?? 'Backstage';

const [state, setState] = useState<ConsentState>({ status: 'loading' });

useEffect(() => {
if (!sessionId) {
setState({ status: 'error', error: 'No session ID provided' });
return undefined;
}

let cancelled = false;
(async () => {
try {
const baseUrl = await discoveryApi.getBaseUrl('auth');
const response = await fetchApi.fetch(
`${baseUrl}/v1/sessions/${sessionId}`,
);

if (cancelled) return;
if (!response.ok) {
const text = await response.text();
setState({ status: 'error', error: text || response.statusText });
return;
}

const session: Session = await response.json();
setState({ status: 'loaded', session });
} catch (e: unknown) {
if (cancelled) return;
const message = e instanceof Error ? e.message : String(e);

Check warning on line 130 in workspaces/x2a/packages/app/src/components/oauth2/ConsentPage.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'e' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZz8fCbQQ7nsx2IeVClO&open=AZz8fCbQQ7nsx2IeVClO&pullRequest=2565
setState({ status: 'error', error: message });
}
})();

return () => {
cancelled = true;
};
}, [sessionId, discoveryApi, fetchApi]);

const handleAction = useCallback(
async (action: 'approve' | 'reject') => {
if (state.status !== 'loaded') return;

setState({ status: 'submitting', session: state.session, action });

try {
const baseUrl = await discoveryApi.getBaseUrl('auth');
const response = await fetchApi.fetch(
`${baseUrl}/v1/sessions/${sessionId}/${action}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' } },
);

if (!response.ok) {
const text = await response.text();
setState({ status: 'loaded', session: state.session });
throw new Error(text || response.statusText);
}

const result = await response.json();
setState({ status: 'completed', action });

if (result.redirectUrl) {
window.location.href = result.redirectUrl;

Check warning on line 163 in workspaces/x2a/packages/app/src/components/oauth2/ConsentPage.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZz8fCbQQ7nsx2IeVClP&open=AZz8fCbQQ7nsx2IeVClP&pullRequest=2565
}
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);

Check warning on line 166 in workspaces/x2a/packages/app/src/components/oauth2/ConsentPage.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'e' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZz8fCbQQ7nsx2IeVClQ&open=AZz8fCbQQ7nsx2IeVClQ&pullRequest=2565
setState({ status: 'error', error: message });
}
},
[state, sessionId, discoveryApi, fetchApi],
);

const headerTitle = getHeaderTitle(state);

return (
<Page themeId="tool">
<Header title={headerTitle} />
<Content>
{state.status === 'loading' && (
<Box display="flex" justifyContent="center" height={300}>
<Progress />
</Box>
)}

{state.status === 'error' && (
<ResponseErrorPanel
title="Authorization Error"
error={new Error(state.error)}
/>
)}

{state.status === 'completed' && (
<Typography variant="h5" align="center" style={{ marginTop: 32 }}>
{state.action === 'approve'
? `You have successfully authorized the application to access your ${appTitle} account. Redirecting...`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All strings must be internationalized

: `You have denied the application access to your ${appTitle} account.`}
</Typography>
)}

{(state.status === 'loaded' || state.status === 'submitting') && (
<Card className={classes.card}>
<CardContent>
<Box className={classes.appHeader}>
<Box>
<Typography variant="h6">
{state.session.clientName ?? state.session.clientId}
</Typography>
<Typography variant="body2" color="textSecondary">
wants to access your {appTitle} account
</Typography>
</Box>
</Box>
<Divider />
<Box mt={2}>
<Typography variant="body2">
This will grant the application a token to access {appTitle}{' '}
on your behalf. Only authorize applications you trust.
</Typography>
<Box className={classes.callbackUrl}>
{state.session.redirectUri}
</Box>
</Box>
</CardContent>
<CardActions className={classes.actions}>
<Button
variant="outlined"
color="secondary"
disabled={state.status === 'submitting'}
onClick={() => handleAction('reject')}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
disabled={state.status === 'submitting'}
onClick={() => handleAction('approve')}
>
{state.status === 'submitting' && state.action === 'approve'
? 'Authorizing...'
: 'Authorize'}
</Button>
</CardActions>
</Card>
)}
</Content>
</Page>
);
};
1 change: 1 addition & 0 deletions workspaces/x2a/packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@backstage/plugin-catalog-backend-module-logs": "^0.1.16",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.14",
"@backstage/plugin-kubernetes-backend": "^0.20.4",
"@backstage/plugin-mcp-actions-backend": "^0.1.9",
"@backstage/plugin-notifications-backend": "^0.6.0",
"@backstage/plugin-permission-backend": "^0.7.8",
"@backstage/plugin-permission-common": "^0.9.3",
Expand Down
3 changes: 3 additions & 0 deletions workspaces/x2a/packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ backend.add(import('@backstage/plugin-kubernetes-backend'));
backend.add(import('@backstage/plugin-notifications-backend'));
backend.add(import('@backstage/plugin-signals-backend'));

// mcp plugin
backend.add(import('@backstage/plugin-mcp-actions-backend'));

backend.add(import('@red-hat-developer-hub/backstage-plugin-x2a-backend'));

backend.add(
Expand Down
Loading
Loading