diff --git a/.github/workflows/techdocs.yml b/.github/workflows/techdocs.yml new file mode 100644 index 0000000000..6d6a6e904e --- /dev/null +++ b/.github/workflows/techdocs.yml @@ -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 diff --git a/packages/catalog-model/examples/artist-lookup-component.yaml b/packages/catalog-model/examples/artist-lookup-component.yaml index bb7d8d5767..448dd13fa3 100644 --- a/packages/catalog-model/examples/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/artist-lookup-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: experimental - owner: tools@example.com + owner: artists@example.com diff --git a/packages/catalog-model/examples/playback-order-component.yaml b/packages/catalog-model/examples/playback-order-component.yaml index fa9f68e77a..4f93b65edf 100644 --- a/packages/catalog-model/examples/playback-order-component.yaml +++ b/packages/catalog-model/examples/playback-order-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: production - owner: tools@example.com + owner: guest diff --git a/packages/catalog-model/examples/podcast-api-component.yaml b/packages/catalog-model/examples/podcast-api-component.yaml index 2a27a0a2c5..19f9a3d4de 100644 --- a/packages/catalog-model/examples/podcast-api-component.yaml +++ b/packages/catalog-model/examples/podcast-api-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: experimental - owner: tools@example.com + owner: players@example.com diff --git a/packages/catalog-model/examples/searcher-component.yaml b/packages/catalog-model/examples/searcher-component.yaml index d82f6e42c7..33d765117c 100644 --- a/packages/catalog-model/examples/searcher-component.yaml +++ b/packages/catalog-model/examples/searcher-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: production - owner: tools@example.com + owner: guest diff --git a/packages/catalog-model/examples/shuffle-api-component.yaml b/packages/catalog-model/examples/shuffle-api-component.yaml index ed0517835c..275c48d6ba 100644 --- a/packages/catalog-model/examples/shuffle-api-component.yaml +++ b/packages/catalog-model/examples/shuffle-api-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: production - owner: tools@example.com + owner: guest diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index ccd40e66ce..d5c0e9c0a4 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -173,7 +173,7 @@ export type ProfileInfo = { /** * Email ID. */ - email: string; + email?: string; /** * Display name that can be presented to the user. diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 4641dd7b50..ddf03034dc 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -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 { - return this.sessionStateTracker.observable; + return this.sessionManager.sessionState$(); } constructor(private readonly sessionManager: SessionManager) {} 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 { 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 { diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index db26d2d9a4..594116fc1a 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -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 { - return this.sessionStateTracker.observable; + return this.sessionManager.sessionState$(); } constructor(private readonly sessionManager: SessionManager) {} @@ -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 { 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; } diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts index 91e55c3a48..4572b923dd 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts @@ -38,6 +38,7 @@ class LocalStorage { class MockManager implements SessionManager { 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(); + }); }); diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts index 8d318f5bff..3e3a40d02a 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -82,6 +82,10 @@ export class AuthSessionStore implements SessionManager { await this.manager.removeSession(); } + sessionState$() { + return this.manager.sessionState$(); + } + private loadSession(): T | undefined { try { const sessionJson = localStorage.getItem(this.storageKey); diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index 793c6f1708..12e7c0a978 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -15,6 +15,7 @@ */ import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; +import { SessionState } from '../../apis'; const defaultOptions = { sessionScopes: (session: { scopes: Set }) => 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 }, diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index f7d5bcf7ca..e46c505c12 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -22,6 +22,7 @@ import { } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper, hasScopes } from './common'; +import { SessionStateTracker } from './SessionStateTracker'; type Options = { /** The connector used for acting on the auth session */ @@ -43,6 +44,7 @@ export class RefreshingAuthSessionManager implements SessionManager { private readonly helper: SessionScopeHelper; private readonly sessionScopesFunc: SessionScopesFunc; private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc; + private readonly stateTracker = new SessionStateTracker(); private refreshPromise?: Promise; private currentSession: T | undefined; @@ -109,16 +111,18 @@ export class RefreshingAuthSessionManager implements SessionManager { ...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 { @@ -129,7 +133,9 @@ export class RefreshingAuthSessionManager implements SessionManager { 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; } diff --git a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts index de308acb0c..c4260bb31d 100644 --- a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts +++ b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts @@ -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.SignedOut); + private readonly subject = new BehaviorSubject( + 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 { + return this.subject; + } } diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts index 5ecbbc0c4e..e59c11421c 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -17,6 +17,7 @@ import { SessionManager, GetSessionOptions } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper } from './common'; +import { SessionStateTracker } from './SessionStateTracker'; type Options = { /** The connector used for acting on the auth session */ @@ -33,6 +34,7 @@ type Options = { export class StaticAuthSessionManager implements SessionManager { private readonly connector: AuthConnector; private readonly helper: SessionScopeHelper; + private readonly stateTracker = new SessionStateTracker(); private currentSession: T | undefined; @@ -60,11 +62,17 @@ export class StaticAuthSessionManager implements SessionManager { ...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$(); } } diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index d28a751fbf..804c7121e1 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -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 = { getSession(options: GetSessionOptions): Promise; removeSession(): Promise; + + sessionState$(): Observable; }; /** diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx index 101ae7010b..3b854801c9 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx @@ -34,14 +34,16 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ }) => { const ref = useRef(); // 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 = () => ( {displayName[0]} diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts index f92875fd48..f67a91dfcd 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts @@ -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', }, }); diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts index dac31af451..b7179659f9 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.ts @@ -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 = { - ...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 = { - ...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, diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 46a386f419..1167946560 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -57,10 +57,6 @@ export const makeProfileInfo = ( } } - if (!email) { - throw new Error('No email received in profile info'); - } - return { email, picture, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 3ae9858bb1..01652a6d7e 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -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( + async handler(req: express.Request) { + const { response } = await executeFrameHandlerStrategy( req, this._strategy, ); - return { - response: result.response, - }; + return { response }; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index f12b248021..04705b66c1 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -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 { + 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 } }; } } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 81773bf9dc..56d4db79fa 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -100,14 +100,20 @@ export interface OAuthProviderHandlers { */ handler( req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken?: string }>; + ): Promise<{ + response: AuthResponse; + 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; + refresh?( + refreshToken: string, + scope: string, + ): Promise>; /** * (Optional) Sign out of the auth provider. @@ -192,13 +198,10 @@ export type AuthProviderFactory = ( export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity?: BackstageIdentity; }; -export type OAuthResponse = Omit< - AuthResponse, - 'backstageIdentity' ->; +export type OAuthResponse = AuthResponse; 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. */ diff --git a/plugins/techdocs/mkdocs/README.md b/plugins/techdocs/mkdocs/README.md index 4d1c11d7b0..5c2a7725aa 100644 --- a/plugins/techdocs/mkdocs/README.md +++ b/plugins/techdocs/mkdocs/README.md @@ -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. diff --git a/plugins/techdocs/mkdocs/container/Dockerfile b/plugins/techdocs/mkdocs/container/Dockerfile index 1ae66589b6..f2a92287e4 100644 --- a/plugins/techdocs/mkdocs/container/Dockerfile +++ b/plugins/techdocs/mkdocs/container/Dockerfile @@ -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 diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/.gitignore b/plugins/techdocs/mkdocs/container/techdocs-core/.gitignore new file mode 100644 index 0000000000..cd5c4a44f3 --- /dev/null +++ b/plugins/techdocs/mkdocs/container/techdocs-core/.gitignore @@ -0,0 +1,2 @@ +.tox +*.egg-info diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/README.md b/plugins/techdocs/mkdocs/container/techdocs-core/README.md new file mode 100644 index 0000000000..143acf3c3b --- /dev/null +++ b/plugins/techdocs/mkdocs/container/techdocs-core/README.md @@ -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. diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt b/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt index b3667f94ec..2a13d1a9da 100644 --- a/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt +++ b/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt @@ -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 diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/setup.py b/plugins/techdocs/mkdocs/container/techdocs-core/setup.py index faa8b8c46e..a259ae0615 100644 --- a/plugins/techdocs/mkdocs/container/techdocs-core/setup.py +++ b/plugins/techdocs/mkdocs/container/techdocs-core/setup.py @@ -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 diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py b/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py index e42a60d28b..18c22c40e3 100644 --- a/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py +++ b/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py @@ -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 diff --git a/plugins/techdocs/mkdocs/mock-docs/docs/index.md b/plugins/techdocs/mkdocs/mock-docs/docs/index.md index bc437e6180..e8ba4e2d6d 100644 --- a/plugins/techdocs/mkdocs/mock-docs/docs/index.md +++ b/plugins/techdocs/mkdocs/mock-docs/docs/index.md @@ -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. diff --git a/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml b/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml index b14d82c4de..fca8418d12 100644 --- a/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml +++ b/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml @@ -2,7 +2,7 @@ site_name: 'mock-docs' nav: - Home: index.md + - SubDocs: '!include ./sub-docs/mkdocs.yml' plugins: - techdocs-core - diff --git a/plugins/techdocs/mkdocs/mock-docs/sub-docs/docs/index.md b/plugins/techdocs/mkdocs/mock-docs/sub-docs/docs/index.md new file mode 100644 index 0000000000..65c6644ef8 --- /dev/null +++ b/plugins/techdocs/mkdocs/mock-docs/sub-docs/docs/index.md @@ -0,0 +1 @@ +### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin) diff --git a/plugins/techdocs/mkdocs/mock-docs/sub-docs/mkdocs.yml b/plugins/techdocs/mkdocs/mock-docs/sub-docs/mkdocs.yml new file mode 100644 index 0000000000..09504c1b31 --- /dev/null +++ b/plugins/techdocs/mkdocs/mock-docs/sub-docs/mkdocs.yml @@ -0,0 +1,4 @@ +site_name: subdocs + +nav: + - Home 2: "index.md"