Merge branch 'master' of github.com:spotify/backstage into mob/cookiecutter-templater
* 'master' of github.com:spotify/backstage: (43 commits) fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup fixup ...
This commit is contained in:
@@ -23,13 +23,9 @@ import {
|
||||
verifyNonce,
|
||||
OAuthProvider,
|
||||
} from './OAuthProvider';
|
||||
import {
|
||||
WebMessageResponse,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
} from '../providers/types';
|
||||
import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types';
|
||||
|
||||
const mockResponseData: OAuthResponse = {
|
||||
const mockResponseData = {
|
||||
providerInfo: {
|
||||
accessToken: 'ACCESS_TOKEN',
|
||||
idToken: 'ID_TOKEN',
|
||||
@@ -39,6 +35,9 @@ const mockResponseData: OAuthResponse = {
|
||||
profile: {
|
||||
email: 'foo@bar.com',
|
||||
},
|
||||
backstageIdentity: {
|
||||
id: 'foo',
|
||||
},
|
||||
};
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
@@ -350,7 +349,7 @@ describe('OAuthProvider', () => {
|
||||
expect(mockResponse.send).toHaveBeenCalledWith({
|
||||
...mockResponseData,
|
||||
backstageIdentity: {
|
||||
id: mockResponseData.profile.email,
|
||||
id: mockResponseData.backstageIdentity.id,
|
||||
idToken: 'my-id-token',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,10 +18,10 @@ import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
AuthResponse,
|
||||
AuthProviderRouteHandlers,
|
||||
OAuthProviderHandlers,
|
||||
WebMessageResponse,
|
||||
BackstageIdentity,
|
||||
} from '../providers/types';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { TokenIssuer } from '../identity';
|
||||
@@ -147,19 +147,12 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
this.setRefreshTokenCookie(res, refreshToken);
|
||||
}
|
||||
|
||||
const id = response.profile.email;
|
||||
const idToken = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: id },
|
||||
});
|
||||
const fullResponse: AuthResponse<unknown> = {
|
||||
...response,
|
||||
backstageIdentity: { id, idToken },
|
||||
};
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, this.options.appOrigin, {
|
||||
type: 'authorization_response',
|
||||
response: fullResponse,
|
||||
response,
|
||||
});
|
||||
} catch (error) {
|
||||
// post error message back to popup if failure
|
||||
@@ -213,21 +206,30 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
// get new access_token
|
||||
const response = await this.providerHandlers.refresh(refreshToken, scope);
|
||||
|
||||
const id = response.profile.email;
|
||||
const idToken = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: id },
|
||||
});
|
||||
const fullResponse: AuthResponse<unknown> = {
|
||||
...response,
|
||||
backstageIdentity: { id, idToken },
|
||||
};
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
res.send(fullResponse);
|
||||
res.send(response);
|
||||
} catch (error) {
|
||||
res.status(401).send(`${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the response from the OAuth provider includes a Backstage identity, we
|
||||
* make sure it's populated with all the information we can derive from the user ID.
|
||||
*/
|
||||
private async populateIdentity(identity?: BackstageIdentity) {
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identity.idToken) {
|
||||
identity.idToken = await this.options.tokenIssuer.issueToken({
|
||||
claims: { sub: identity.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setNonceCookie = (res: express.Response, nonce: string) => {
|
||||
res.cookie(`${this.options.providerId}-nonce`, nonce, {
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
|
||||
@@ -57,10 +57,6 @@ export const makeProfileInfo = (
|
||||
}
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
throw new Error('No email received in profile info');
|
||||
}
|
||||
|
||||
return {
|
||||
email,
|
||||
picture,
|
||||
|
||||
@@ -72,15 +72,13 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
return await executeRedirectStrategy(req, this._strategy, options);
|
||||
}
|
||||
|
||||
async handler(req: express.Request): Promise<{ response: OAuthResponse }> {
|
||||
const result = await executeFrameHandlerStrategy<OAuthResponse>(
|
||||
async handler(req: express.Request) {
|
||||
const { response } = await executeFrameHandlerStrategy<OAuthResponse>(
|
||||
req,
|
||||
this._strategy,
|
||||
);
|
||||
|
||||
return {
|
||||
response: result.response,
|
||||
};
|
||||
return { response };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,14 +97,14 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
async handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken: string }> {
|
||||
const result = await executeFrameHandlerStrategy<
|
||||
const { response, privateInfo } = await executeFrameHandlerStrategy<
|
||||
OAuthResponse,
|
||||
PrivateInfo
|
||||
>(req, this._strategy);
|
||||
|
||||
return {
|
||||
response: result.response,
|
||||
refreshToken: result.privateInfo.refreshToken,
|
||||
response: await this.populateIdentity(response),
|
||||
refreshToken: privateInfo.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
params.id_token,
|
||||
);
|
||||
|
||||
return {
|
||||
return this.populateIdentity({
|
||||
providerInfo: {
|
||||
accessToken,
|
||||
idToken: params.id_token,
|
||||
@@ -129,7 +129,22 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
scope: params.scope,
|
||||
},
|
||||
profile,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async populateIdentity(
|
||||
response: OAuthResponse,
|
||||
): Promise<OAuthResponse> {
|
||||
const { profile } = response;
|
||||
|
||||
if (!profile.email) {
|
||||
throw new Error('Google 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 } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,14 +100,20 @@ export interface OAuthProviderHandlers {
|
||||
*/
|
||||
handler(
|
||||
req: express.Request,
|
||||
): Promise<{ response: OAuthResponse; refreshToken?: string }>;
|
||||
): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* (Optional) Given a refresh token and scope fetches a new access token from the auth provider.
|
||||
* @param {string} refreshToken
|
||||
* @param {string} scope
|
||||
*/
|
||||
refresh?(refreshToken: string, scope: string): Promise<OAuthResponse>;
|
||||
refresh?(
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
): Promise<AuthResponse<OAuthProviderInfo>>;
|
||||
|
||||
/**
|
||||
* (Optional) Sign out of the auth provider.
|
||||
@@ -192,13 +198,10 @@ export type AuthProviderFactory = (
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
profile: ProfileInfo;
|
||||
backstageIdentity: BackstageIdentity;
|
||||
backstageIdentity?: BackstageIdentity;
|
||||
};
|
||||
|
||||
export type OAuthResponse = Omit<
|
||||
AuthResponse<OAuthProviderInfo>,
|
||||
'backstageIdentity'
|
||||
>;
|
||||
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
|
||||
|
||||
export type BackstageIdentity = {
|
||||
/**
|
||||
@@ -209,7 +212,7 @@ export type BackstageIdentity = {
|
||||
/**
|
||||
* An ID token that can be used to authenticate the user within Backstage.
|
||||
*/
|
||||
idToken: string;
|
||||
idToken?: string;
|
||||
};
|
||||
|
||||
export type OAuthProviderInfo = {
|
||||
@@ -279,7 +282,7 @@ export type ProfileInfo = {
|
||||
/**
|
||||
* Email ID of the signed in user.
|
||||
*/
|
||||
email: string;
|
||||
email?: string;
|
||||
/**
|
||||
* Display name that can be presented to the signed in user.
|
||||
*/
|
||||
|
||||
@@ -6,8 +6,10 @@ Welcome to MkDocs. This is the TechDocs implementation of MkDocs.
|
||||
|
||||
## Getting started
|
||||
|
||||
```
|
||||
docker build ./container -t mkdocs-container
|
||||
```bash
|
||||
docker build ./container -t mkdocs-container
|
||||
|
||||
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000
|
||||
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Then open up `http://localhost:8000` on your local machine.
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
|
||||
# Copyright 2020 Spotify AB
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
# 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.
|
||||
|
||||
FROM python:3.7.7-alpine3.12
|
||||
|
||||
RUN apk update && apk --no-cache add gcc musl-dev
|
||||
RUN pip install mkdocs==1.1.2 mkdocs-material==5.3.2
|
||||
RUN pip install --upgrade pip && pip install mkdocs==1.1.2 mkdocs-material==5.3.2 mkdocs-monorepo-plugin==0.4.5 pymdown-extensions==7.1
|
||||
|
||||
ADD ./techdocs-core /techdocs-core
|
||||
RUN pip install --no-index /techdocs-core
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.tox
|
||||
*.egg-info
|
||||
@@ -0,0 +1,45 @@
|
||||
# techdocs-core
|
||||
|
||||
This is the base [Mkdocs](https://mkdocs.org) plugin used when using Mkdocs with Spotify's TechDocs. It is written in Python and packages all of our Mkdocs defaults, such as theming, plugins, etc in a single plugin.
|
||||
|
||||
## Usage
|
||||
|
||||
**Installation instructions TBD.** We haven't published it to a Python registry yet.
|
||||
|
||||
Once you have installed the `mkdocs-techdocs-core` plugin, you'll need to add it to your `mkdocs.yml`.
|
||||
|
||||
```yaml
|
||||
site_name: Backstage Docs
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Developing a Plugin: developing-a-plugin.md
|
||||
|
||||
plugins:
|
||||
- techdocs-core
|
||||
```
|
||||
|
||||
## Running Locally
|
||||
|
||||
You can install this package locally using `pip` and the `--editable` flag used for making developing Python packages.
|
||||
|
||||
```bash
|
||||
pip install --editable .
|
||||
```
|
||||
|
||||
You'll then have the `techdocs-core` package available to use in Mkdocs and `pip` will point the dependency to this folder.
|
||||
|
||||
## Running with Docker
|
||||
|
||||
In the parent `Dockerfile` we add this folder to the build and install the package locally in the container. In the future, we'll probably move away from this approach and have it download directly from a Python registry (and this folder will publish to one).
|
||||
|
||||
See the `README.md` located in the `mkdocs/` folder for more details on how to build and run the Docker container.
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python -m black src/
|
||||
```
|
||||
|
||||
**Note:** This will write to all Python files in `src/` with the formatted code. If you would like to only check to see if it passes, simply append the `--check` flag.
|
||||
@@ -1 +1,9 @@
|
||||
# The "base" version of the Mkdocs project.
|
||||
# Note: if you update this, also update `install_requires` in setup.py
|
||||
# https://github.com/mkdocs/mkdocs
|
||||
mkdocs==1.1.2
|
||||
|
||||
# The linter using for Python
|
||||
# Note: This requires Python 3.6+ to run, but can format Python 2 code too.
|
||||
# https://github.com/psf/black
|
||||
black==19.10b0
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"""
|
||||
* 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.
|
||||
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.
|
||||
"""
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
@@ -19,17 +19,67 @@ from mkdocs.theme import Theme
|
||||
|
||||
from mkdocs.contrib.search import SearchPlugin
|
||||
|
||||
from mkdocs_monorepo_plugin.plugin import MonorepoPlugin
|
||||
|
||||
|
||||
class TechDocsCore(BasePlugin):
|
||||
def on_config(self, config):
|
||||
# Theme
|
||||
config['theme'] = Theme(name="material")
|
||||
# Theme
|
||||
config["theme"] = Theme(name="material")
|
||||
|
||||
# Plugins
|
||||
del config['plugins']['techdocs-core']
|
||||
# Plugins
|
||||
del config["plugins"]["techdocs-core"]
|
||||
|
||||
search_plugin = SearchPlugin()
|
||||
search_plugin.load_config({})
|
||||
config['plugins']['search'] = search_plugin
|
||||
search_plugin = SearchPlugin()
|
||||
search_plugin.load_config({})
|
||||
|
||||
return config
|
||||
monorepo_plugin = MonorepoPlugin()
|
||||
monorepo_plugin.load_config({})
|
||||
|
||||
config["plugins"]["search"] = search_plugin
|
||||
config["plugins"]["monorepo"] = monorepo_plugin
|
||||
|
||||
search_plugin = SearchPlugin()
|
||||
search_plugin.load_config({})
|
||||
config["plugins"]["search"] = search_plugin
|
||||
|
||||
# Markdown Extensions
|
||||
config['markdown_extensions'].append('admonition')
|
||||
config['markdown_extensions'].append('abbr')
|
||||
config['markdown_extensions'].append('attr_list')
|
||||
config['markdown_extensions'].append('def_list')
|
||||
config['markdown_extensions'].append('codehilite')
|
||||
config['mdx_configs']['codehilite'] = {
|
||||
'linenums': True,
|
||||
'guess_lang': False,
|
||||
'pygments_style': 'friendly',
|
||||
}
|
||||
config['markdown_extensions'].append('toc')
|
||||
config['mdx_configs']['toc'] = {
|
||||
'permalink': True,
|
||||
}
|
||||
config['markdown_extensions'].append('footnotes')
|
||||
config['markdown_extensions'].append('markdown.extensions.tables')
|
||||
config['markdown_extensions'].append('pymdownx.betterem')
|
||||
config['mdx_configs']['pymdownx.betterem'] = {
|
||||
'smart_enable': 'all',
|
||||
}
|
||||
config['markdown_extensions'].append('pymdownx.caret')
|
||||
config['markdown_extensions'].append('pymdownx.critic')
|
||||
config['markdown_extensions'].append('pymdownx.details')
|
||||
config['markdown_extensions'].append('pymdownx.emoji')
|
||||
config['mdx_configs']['pymdownx.emoji'] = {
|
||||
'emoji_generator': '!!python/name:pymdownx.emoji.to_svg',
|
||||
}
|
||||
config['markdown_extensions'].append('pymdownx.inlinehilite')
|
||||
config['markdown_extensions'].append('pymdownx.magiclink')
|
||||
config['markdown_extensions'].append('pymdownx.mark')
|
||||
config['markdown_extensions'].append('pymdownx.smartsymbols')
|
||||
config['markdown_extensions'].append('pymdownx.superfences')
|
||||
config['markdown_extensions'].append('pymdownx.tasklist')
|
||||
config['mdx_configs']['pymdownx.tasklist'] = {
|
||||
'custom_checkbox': True,
|
||||
}
|
||||
config['markdown_extensions'].append('pymdownx.tilde')
|
||||
|
||||
return config
|
||||
|
||||
@@ -1 +1,32 @@
|
||||
## hello mock docs
|
||||
|
||||
!!! test
|
||||
Testing somethin
|
||||
|
||||
Some text about MOCDOC
|
||||
|
||||
\*[MOCDOC]: Mock Documentation
|
||||
|
||||
This is a paragraph.
|
||||
{: #test_id .test_class }
|
||||
|
||||
Apple
|
||||
: Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.
|
||||
|
||||
```javascript
|
||||
import { test } from 'something';
|
||||
|
||||
const addThingToThing = (a, b) a + b;
|
||||
```
|
||||
|
||||
- [abc](#abc)
|
||||
- [xyz](#xyz)
|
||||
|
||||
## abc
|
||||
|
||||
This is a b c.
|
||||
|
||||
## xyz
|
||||
|
||||
This is x y z.
|
||||
|
||||
@@ -2,7 +2,7 @@ site_name: 'mock-docs'
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- SubDocs: '!include ./sub-docs/mkdocs.yml'
|
||||
|
||||
plugins:
|
||||
- techdocs-core
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)
|
||||
@@ -0,0 +1,4 @@
|
||||
site_name: subdocs
|
||||
|
||||
nav:
|
||||
- Home 2: "index.md"
|
||||
Reference in New Issue
Block a user