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:
@@ -0,0 +1,35 @@
|
||||
name: TechDocs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/techdocs.yml'
|
||||
- 'packages/techdocs-cli/**'
|
||||
- 'plugins/techdocs/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: [3.7]
|
||||
|
||||
env:
|
||||
TECHDOCS_CORE_PATH: ./plugins/techdocs/mkdocs/container/techdocs-core
|
||||
|
||||
name: Python ${{ matrix.node-version }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Lint Python code for techdocs-core package
|
||||
- name: prepare python environment
|
||||
run: |
|
||||
python3 -m pip install --index-url https://pypi.org/simple/ setuptools
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --index-url https://pypi.org/simple/ -r $TECHDOCS_CORE_PATH/requirements.txt
|
||||
|
||||
- name: lint techdocs-core package
|
||||
run: |
|
||||
python3 -m black --check $TECHDOCS_CORE_PATH/src
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
owner: tools@example.com
|
||||
owner: artists@example.com
|
||||
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: tools@example.com
|
||||
owner: guest
|
||||
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
owner: tools@example.com
|
||||
owner: players@example.com
|
||||
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: tools@example.com
|
||||
owner: guest
|
||||
|
||||
@@ -6,4 +6,4 @@ metadata:
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: tools@example.com
|
||||
owner: guest
|
||||
|
||||
@@ -173,7 +173,7 @@ export type ProfileInfo = {
|
||||
/**
|
||||
* Email ID.
|
||||
*/
|
||||
email: string;
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Display name that can be presented to the user.
|
||||
|
||||
@@ -29,7 +29,6 @@ import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
|
||||
@@ -95,44 +94,34 @@ class GithubAuth implements OAuthApi, SessionStateApi {
|
||||
return new GithubAuth(sessionManager);
|
||||
}
|
||||
|
||||
private readonly sessionStateTracker = new SessionStateTracker();
|
||||
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.sessionStateTracker.observable;
|
||||
return this.sessionManager.sessionState$();
|
||||
}
|
||||
|
||||
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
|
||||
|
||||
async getAccessToken(scope?: string, options?: AuthRequestOptions) {
|
||||
const normalizedScopes = GithubAuth.normalizeScope(scope);
|
||||
const session = await this.sessionManager.getSession({
|
||||
...options,
|
||||
scopes: normalizedScopes,
|
||||
scopes: GithubAuth.normalizeScope(scope),
|
||||
});
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
if (session) {
|
||||
return session.providerInfo.accessToken;
|
||||
}
|
||||
return '';
|
||||
return session?.providerInfo.accessToken ?? '';
|
||||
}
|
||||
|
||||
async getBackstageIdentity(
|
||||
options: AuthRequestOptions = {},
|
||||
): Promise<BackstageIdentity | undefined> {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
return session?.backstageIdentity;
|
||||
}
|
||||
|
||||
async getProfile(options: AuthRequestOptions = {}) {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
return session?.profile;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.sessionManager.removeSession();
|
||||
this.sessionStateTracker.setIsSignedId(false);
|
||||
}
|
||||
|
||||
static normalizeScope(scope?: string): Set<string> {
|
||||
|
||||
@@ -32,7 +32,6 @@ import { OAuthRequestApi, AuthProvider } from '../../../definitions';
|
||||
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
|
||||
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
|
||||
import { Observable } from '../../../../types';
|
||||
import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker';
|
||||
|
||||
type CreateOptions = {
|
||||
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
|
||||
@@ -117,10 +116,8 @@ class GoogleAuth
|
||||
return new GoogleAuth(sessionManager);
|
||||
}
|
||||
|
||||
private readonly sessionStateTracker = new SessionStateTracker();
|
||||
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.sessionStateTracker.observable;
|
||||
return this.sessionManager.sessionState$();
|
||||
}
|
||||
|
||||
constructor(private readonly sessionManager: SessionManager<GoogleSession>) {}
|
||||
@@ -129,43 +126,31 @@ class GoogleAuth
|
||||
scope?: string | string[],
|
||||
options?: AuthRequestOptions,
|
||||
) {
|
||||
const normalizedScopes = GoogleAuth.normalizeScopes(scope);
|
||||
const session = await this.sessionManager.getSession({
|
||||
...options,
|
||||
scopes: normalizedScopes,
|
||||
scopes: GoogleAuth.normalizeScopes(scope),
|
||||
});
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
if (session) {
|
||||
return session.providerInfo.accessToken;
|
||||
}
|
||||
return '';
|
||||
return session?.providerInfo.accessToken ?? '';
|
||||
}
|
||||
|
||||
async getIdToken(options: AuthRequestOptions = {}) {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
if (session) {
|
||||
return session.providerInfo.idToken;
|
||||
}
|
||||
return '';
|
||||
return session?.providerInfo.idToken ?? '';
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.sessionManager.removeSession();
|
||||
this.sessionStateTracker.setIsSignedId(false);
|
||||
}
|
||||
|
||||
async getBackstageIdentity(
|
||||
options: AuthRequestOptions = {},
|
||||
): Promise<BackstageIdentity | undefined> {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
return session?.backstageIdentity;
|
||||
}
|
||||
|
||||
async getProfile(options: AuthRequestOptions = {}) {
|
||||
const session = await this.sessionManager.getSession(options);
|
||||
this.sessionStateTracker.setIsSignedId(!!session);
|
||||
return session?.profile;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class LocalStorage {
|
||||
class MockManager implements SessionManager<string> {
|
||||
getSession = jest.fn();
|
||||
removeSession = jest.fn();
|
||||
sessionState$ = jest.fn();
|
||||
}
|
||||
|
||||
describe('GheAuth AuthSessionStore', () => {
|
||||
@@ -119,4 +120,11 @@ describe('GheAuth AuthSessionStore', () => {
|
||||
|
||||
expect(localStorage.getItem('my-key')).toBe(null);
|
||||
});
|
||||
|
||||
it('should forward sessionState calls', () => {
|
||||
const manager = new MockManager();
|
||||
const store = new AuthSessionStore({ manager, ...defaultOptions });
|
||||
store.sessionState$();
|
||||
expect(manager.sessionState$).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,10 @@ export class AuthSessionStore<T> implements SessionManager<T> {
|
||||
await this.manager.removeSession();
|
||||
}
|
||||
|
||||
sessionState$() {
|
||||
return this.manager.sessionState$();
|
||||
}
|
||||
|
||||
private loadSession(): T | undefined {
|
||||
try {
|
||||
const sessionJson = localStorage.getItem(this.storageKey);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager';
|
||||
import { SessionState } from '../../apis';
|
||||
|
||||
const defaultOptions = {
|
||||
sessionScopes: (session: { scopes: Set<string> }) => session.scopes,
|
||||
@@ -22,21 +23,44 @@ const defaultOptions = {
|
||||
};
|
||||
|
||||
describe('RefreshingAuthSessionManager', () => {
|
||||
it('should save result form createSession', async () => {
|
||||
it('should save result from createSession', async () => {
|
||||
const createSession = jest.fn().mockResolvedValue({ expired: false });
|
||||
const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE'));
|
||||
const removeSession = jest.fn();
|
||||
const manager = new RefreshingAuthSessionManager({
|
||||
connector: { createSession, refreshSession },
|
||||
connector: { createSession, refreshSession, removeSession },
|
||||
...defaultOptions,
|
||||
} as any);
|
||||
const stateSubscriber = jest.fn();
|
||||
manager.sessionState$().subscribe(stateSubscriber);
|
||||
|
||||
await Promise.resolve(); // Wait a tick for observer to post a value
|
||||
|
||||
expect(stateSubscriber.mock.calls).toEqual([[SessionState.SignedOut]]);
|
||||
await manager.getSession({});
|
||||
expect(createSession).toBeCalledTimes(1);
|
||||
|
||||
expect(stateSubscriber.mock.calls).toEqual([
|
||||
[SessionState.SignedOut],
|
||||
[SessionState.SignedIn],
|
||||
]);
|
||||
await manager.getSession({});
|
||||
expect(createSession).toBeCalledTimes(1);
|
||||
|
||||
expect(refreshSession).toBeCalledTimes(1);
|
||||
expect(stateSubscriber.mock.calls).toEqual([
|
||||
[SessionState.SignedOut],
|
||||
[SessionState.SignedIn],
|
||||
]);
|
||||
|
||||
expect(removeSession).toHaveBeenCalledTimes(0);
|
||||
await manager.removeSession();
|
||||
expect(removeSession).toHaveBeenCalledTimes(1);
|
||||
expect(stateSubscriber.mock.calls).toEqual([
|
||||
[SessionState.SignedOut],
|
||||
[SessionState.SignedIn],
|
||||
[SessionState.SignedOut],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ask consent only if scopes have changed', async () => {
|
||||
@@ -130,7 +154,7 @@ describe('RefreshingAuthSessionManager', () => {
|
||||
expect(refreshSession).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should remove session and reload', async () => {
|
||||
it('should remove session straight away', async () => {
|
||||
const removeSession = jest.fn();
|
||||
const manager = new RefreshingAuthSessionManager({
|
||||
connector: { removeSession },
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from './types';
|
||||
import { AuthConnector } from '../AuthConnector';
|
||||
import { SessionScopeHelper, hasScopes } from './common';
|
||||
import { SessionStateTracker } from './SessionStateTracker';
|
||||
|
||||
type Options<T> = {
|
||||
/** The connector used for acting on the auth session */
|
||||
@@ -43,6 +44,7 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
|
||||
private readonly helper: SessionScopeHelper<T>;
|
||||
private readonly sessionScopesFunc: SessionScopesFunc<T>;
|
||||
private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;
|
||||
private readonly stateTracker = new SessionStateTracker();
|
||||
|
||||
private refreshPromise?: Promise<T>;
|
||||
private currentSession: T | undefined;
|
||||
@@ -109,16 +111,18 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
|
||||
...options,
|
||||
scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),
|
||||
});
|
||||
this.stateTracker.setIsSignedIn(true);
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
async removeSession() {
|
||||
this.currentSession = undefined;
|
||||
await this.connector.removeSession();
|
||||
this.stateTracker.setIsSignedIn(false);
|
||||
}
|
||||
|
||||
async getCurrentSession() {
|
||||
return this.currentSession;
|
||||
sessionState$() {
|
||||
return this.stateTracker.sessionState$();
|
||||
}
|
||||
|
||||
private async collapsedSessionRefresh(): Promise<T> {
|
||||
@@ -129,7 +133,9 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
|
||||
this.refreshPromise = this.connector.refreshSession();
|
||||
|
||||
try {
|
||||
return await this.refreshPromise;
|
||||
const session = await this.refreshPromise;
|
||||
this.stateTracker.setIsSignedIn(true);
|
||||
return session;
|
||||
} finally {
|
||||
delete this.refreshPromise;
|
||||
}
|
||||
|
||||
@@ -16,17 +16,25 @@
|
||||
|
||||
import { BehaviorSubject } from '..';
|
||||
import { SessionState } from '../../apis';
|
||||
import { Observable } from '../../types';
|
||||
|
||||
export class SessionStateTracker {
|
||||
private signedIn: boolean = false;
|
||||
observable = new BehaviorSubject<SessionState>(SessionState.SignedOut);
|
||||
private readonly subject = new BehaviorSubject<SessionState>(
|
||||
SessionState.SignedOut,
|
||||
);
|
||||
|
||||
setIsSignedId(isSignedIn: boolean) {
|
||||
private signedIn: boolean = false;
|
||||
|
||||
setIsSignedIn(isSignedIn: boolean) {
|
||||
if (this.signedIn !== isSignedIn) {
|
||||
this.signedIn = isSignedIn;
|
||||
this.observable.next(
|
||||
this.subject.next(
|
||||
this.signedIn ? SessionState.SignedIn : SessionState.SignedOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.subject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { SessionManager, GetSessionOptions } from './types';
|
||||
import { AuthConnector } from '../AuthConnector';
|
||||
import { SessionScopeHelper } from './common';
|
||||
import { SessionStateTracker } from './SessionStateTracker';
|
||||
|
||||
type Options<T> = {
|
||||
/** The connector used for acting on the auth session */
|
||||
@@ -33,6 +34,7 @@ type Options<T> = {
|
||||
export class StaticAuthSessionManager<T> implements SessionManager<T> {
|
||||
private readonly connector: AuthConnector<T>;
|
||||
private readonly helper: SessionScopeHelper<T>;
|
||||
private readonly stateTracker = new SessionStateTracker();
|
||||
|
||||
private currentSession: T | undefined;
|
||||
|
||||
@@ -60,11 +62,17 @@ export class StaticAuthSessionManager<T> implements SessionManager<T> {
|
||||
...options,
|
||||
scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),
|
||||
});
|
||||
this.stateTracker.setIsSignedIn(true);
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
async removeSession() {
|
||||
this.currentSession = undefined;
|
||||
await this.connector.removeSession();
|
||||
this.stateTracker.setIsSignedIn(false);
|
||||
}
|
||||
|
||||
sessionState$() {
|
||||
return this.stateTracker.sessionState$();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Observable } from '../../types';
|
||||
import { SessionState } from '../../apis';
|
||||
|
||||
export type GetSessionOptions = {
|
||||
optional?: boolean;
|
||||
instantPopup?: boolean;
|
||||
@@ -29,6 +32,8 @@ export type SessionManager<T> = {
|
||||
getSession(options: GetSessionOptions): Promise<T | undefined>;
|
||||
|
||||
removeSession(): Promise<void>;
|
||||
|
||||
sessionState$(): Observable<SessionState>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,14 +34,16 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
|
||||
}) => {
|
||||
const ref = useRef<Element>(); // for scrolling down when collapse item opens
|
||||
const classes = useStyles();
|
||||
const profile = useApi(identityApiRef).getProfile();
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
|
||||
};
|
||||
|
||||
const displayName = profile.displayName ?? profile.email;
|
||||
const userId = identityApi.getUserId();
|
||||
const profile = identityApi.getProfile();
|
||||
const displayName = profile.displayName ?? userId;
|
||||
const SignInAvatar = () => (
|
||||
<Avatar src={profile.picture} className={classes.avatar}>
|
||||
{displayName[0]}
|
||||
|
||||
@@ -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