Merge branch 'master' of github.com:spotify/backstage into mob/fetching-mock-docs
This commit is contained in:
@@ -40,6 +40,8 @@
|
||||
"passport": "^0.4.1",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-oauth2": "^1.5.0",
|
||||
"passport-okta-oauth": "^0.0.1",
|
||||
"passport-saml": "^1.3.3",
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.2.1",
|
||||
@@ -57,6 +59,7 @@
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"migrations"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import Router from 'express-promise-router';
|
||||
import { createGithubProvider } from './github';
|
||||
import { createGoogleProvider } from './google';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { createOktaProvider } from './okta';
|
||||
import { AuthProviderFactory, AuthProviderConfig } from './types';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
@@ -26,6 +27,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
github: createGithubProvider,
|
||||
saml: createSamlProvider,
|
||||
okta: createOktaProvider,
|
||||
};
|
||||
|
||||
export const createAuthProviderRouter = (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export { createOktaProvider } from './provider';
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 express from 'express';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import { Strategy as OktaStrategy } from 'passport-okta-oauth';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export class OktaAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
private readonly _strategy: any;
|
||||
|
||||
/**
|
||||
* Due to passport-okta-oauth forcing options.state = true,
|
||||
* passport-oauth2 requires express-session to be installed
|
||||
* so that the 'state' parameter of the oauth2 flow can be stored.
|
||||
* This implementation of StateStore matches the NullStore found within
|
||||
* passport-oauth2, which is the StateStore implementation used when options.state = false,
|
||||
* allowing us to avoid using express-session in order to integrate with Okta.
|
||||
*/
|
||||
private _store: StateStore = {
|
||||
store(_req: express.Request, cb: any) {
|
||||
cb(null, null);
|
||||
},
|
||||
verify(_req: express.Request, _state: string, cb: any) {
|
||||
cb(null, true);
|
||||
},
|
||||
}
|
||||
|
||||
constructor(options: OAuthProviderOptions) {
|
||||
this._strategy = new OktaStrategy({
|
||||
passReqToCallback: false as true,
|
||||
...options,
|
||||
store: this._store,
|
||||
response_type: 'code',
|
||||
}, (
|
||||
accessToken: any,
|
||||
refreshToken: any,
|
||||
params: any,
|
||||
rawProfile: passport.Profile,
|
||||
done: PassportDoneCallback<OAuthResponse, PrivateInfo>,
|
||||
) => {
|
||||
const profile = makeProfileInfo(rawProfile, params.id_token);
|
||||
|
||||
done(
|
||||
undefined,
|
||||
{
|
||||
providerInfo: {
|
||||
idToken: params.id_token,
|
||||
accessToken,
|
||||
scope: params.scope,
|
||||
expiresInSeconds: params.expires_in,
|
||||
},
|
||||
profile,
|
||||
},
|
||||
{
|
||||
refreshToken,
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
async start(
|
||||
req: express.Request,
|
||||
options: Record<string, string>
|
||||
): Promise<RedirectInfo> {
|
||||
const providerOptions = {
|
||||
...options,
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
};
|
||||
return await executeRedirectStrategy(req, this._strategy, providerOptions);
|
||||
}
|
||||
|
||||
async handler(
|
||||
req: express.Request
|
||||
): Promise<{ response: OAuthResponse; refreshToken: string }> {
|
||||
const { response, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResponse,
|
||||
PrivateInfo
|
||||
>(req, this._strategy);
|
||||
|
||||
return {
|
||||
response: await this.populateIdentity(response),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(refreshToken: string, scope: string): Promise<OAuthResponse> {
|
||||
const { accessToken, params } = await executeRefreshTokenStrategy(
|
||||
this._strategy,
|
||||
refreshToken,
|
||||
scope,
|
||||
);
|
||||
|
||||
const profile = await executeFetchUserProfileStrategy(
|
||||
this._strategy,
|
||||
accessToken,
|
||||
params.id_token,
|
||||
);
|
||||
|
||||
return this.populateIdentity({
|
||||
providerInfo: {
|
||||
accessToken,
|
||||
idToken: params.id_token,
|
||||
expiresInSeconds: params.expires_in,
|
||||
scope: params.scope,
|
||||
},
|
||||
profile,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private async populateIdentity(
|
||||
response: OAuthResponse,
|
||||
): Promise<OAuthResponse> {
|
||||
const { profile } = response;
|
||||
|
||||
if (!profile.email) {
|
||||
throw new Error('Okta profile contained no email');
|
||||
}
|
||||
|
||||
// TODO(Rugvip): Hardcoded to the local part of the email for now
|
||||
const id = profile.email.split('@')[0];
|
||||
|
||||
return { ...response, backstageIdentity: { id } };
|
||||
}
|
||||
}
|
||||
|
||||
export function createOktaProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const callbackURLParam = `?env=${env}`;
|
||||
const opts = {
|
||||
audience: config.audience,
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/okta/handler/frame${callbackURLParam}`,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId: 'okta',
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(envProviders);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
declare module 'passport-okta-oauth' {
|
||||
|
||||
export class Strategy {
|
||||
constructor(options: any, verify: any)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ export type OAuthProviderConfig = {
|
||||
* Client Secret of the auth provider.
|
||||
*/
|
||||
clientSecret: string;
|
||||
/**
|
||||
* The location of the OAuth Authorization Server
|
||||
*/
|
||||
audience?: string;
|
||||
};
|
||||
|
||||
export type EnvironmentProviderConfig = {
|
||||
|
||||
@@ -82,6 +82,15 @@ export async function createRouter(
|
||||
issuer: 'passport-saml',
|
||||
},
|
||||
},
|
||||
okta: {
|
||||
development: {
|
||||
appOrigin: 'http://localhost:3000',
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_OKTA_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!,
|
||||
audience: process.env.AUTH_OKTA_AUDIENCE,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@ metadata:
|
||||
name: react-ssr-template
|
||||
title: React SSR Template
|
||||
description: Next.js application skeleton for creating isomorphic web applications.
|
||||
tags:
|
||||
- Recommended
|
||||
- React
|
||||
spec:
|
||||
type: cookiecutter
|
||||
processor: cookiecutter
|
||||
type: website
|
||||
path: '.'
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Template
|
||||
metadata:
|
||||
name: springboot-template
|
||||
title: Spring Boot Service
|
||||
description: Standard Spring Boot (Java) microservice with recommended configuration.
|
||||
tags:
|
||||
- Recommended
|
||||
- Java
|
||||
spec:
|
||||
processor: cookiecutter
|
||||
type: service
|
||||
path: '.'
|
||||
@@ -1,8 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl \
|
||||
--location \
|
||||
--request POST 'localhost:7000/catalog/locations' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/sample-templates/react-ssr-template/template.yaml\"}"
|
||||
|
||||
for URL in \
|
||||
'react-ssr-template' \
|
||||
'springboot-template' \
|
||||
; do \
|
||||
curl \
|
||||
--location \
|
||||
--request POST 'localhost:7000/catalog/locations' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/sample-templates/${URL}/template.yaml\"}"
|
||||
done
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.12",
|
||||
"@backstage/core": "^0.1.1-alpha.12",
|
||||
"@backstage/theme": "^0.1.1-alpha.12",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
@@ -29,7 +31,8 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
"react-use": "^14.2.0",
|
||||
"swr": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.12",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Lifecycle,
|
||||
Content,
|
||||
@@ -23,33 +23,39 @@ import {
|
||||
SupportButton,
|
||||
Page,
|
||||
pageTheme,
|
||||
useApi,
|
||||
errorApiRef,
|
||||
} from '@backstage/core';
|
||||
import { Button, Grid, Link, Typography } from '@material-ui/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
Typography,
|
||||
Link,
|
||||
Button,
|
||||
Grid,
|
||||
LinearProgress,
|
||||
} from '@material-ui/core';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import TemplateCard from '../TemplateCard';
|
||||
import useStaleWhileRevalidate from 'swr';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
|
||||
// TODO(blam): Connect to backend
|
||||
const STATIC_DATA = [
|
||||
{
|
||||
id: 'springboot-template',
|
||||
type: 'service',
|
||||
name: 'Spring Boot Service',
|
||||
tags: ['Recommended', 'Java'],
|
||||
description:
|
||||
'Standard Spring Boot (Java) microservice with recommended configuration.',
|
||||
ownerId: 'spotify',
|
||||
},
|
||||
{
|
||||
id: 'react-ssr-template',
|
||||
type: 'website',
|
||||
name: 'SSR React Website',
|
||||
tags: ['Recommended', 'React'],
|
||||
description:
|
||||
'Next.js application skeleton for creating isomorphic web applications.',
|
||||
ownerId: 'spotify',
|
||||
},
|
||||
];
|
||||
const ScaffolderPage: React.FC<{}> = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const { data: templates, isValidating, error } = useStaleWhileRevalidate(
|
||||
'templates/all',
|
||||
async () =>
|
||||
catalogApi.getEntities({ kind: 'Template' }) as Promise<
|
||||
TemplateEntityV1alpha1[]
|
||||
>,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return;
|
||||
errorApi.post(error);
|
||||
}, [error, errorApi]);
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
@@ -84,18 +90,24 @@ const ScaffolderPage: React.FC<{}> = () => {
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
{!templates && isValidating && <LinearProgress />}
|
||||
<Grid container>
|
||||
{STATIC_DATA.map(item => {
|
||||
return (
|
||||
<TemplateCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
type={item.type}
|
||||
description={item.description}
|
||||
tags={item.tags}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{templates &&
|
||||
templates.map(template => {
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TemplateCard
|
||||
key={template.metadata.uid}
|
||||
title={`${
|
||||
(template.metadata.title || template.metadata.name) ?? ''
|
||||
}`}
|
||||
type={template.spec.type ?? ''}
|
||||
description={template.metadata.description ?? '-'}
|
||||
tags={(template.metadata?.tags as string[]) ?? []}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
|
||||
@@ -14,14 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Grid,
|
||||
Typography,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
header: {
|
||||
@@ -59,25 +52,23 @@ const TemplateCard: FC<TemplateCardProps> = ({
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card>
|
||||
<div className={classes.header}>
|
||||
<Typography variant="subtitle2">{type}</Typography>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<Card>
|
||||
<div className={classes.header}>
|
||||
<Typography variant="subtitle2">{type}</Typography>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{tags?.map(tag => (
|
||||
<Chip label={tag} key={tag} />
|
||||
))}
|
||||
<Typography variant="body2" paragraph className={classes.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={classes.footer}>
|
||||
<Button color="primary">Choose</Button>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{tags?.map(tag => (
|
||||
<Chip label={tag} />
|
||||
))}
|
||||
<Typography variant="body2" paragraph className={classes.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={classes.footer}>
|
||||
<Button color="primary">Choose</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user