From 8a779066e59e3d596c1a25381abde0b62a34167f Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 15 Feb 2024 14:43:53 -0800 Subject: [PATCH] add new consumer apis --- src/auth/auth-tsoa.ts | 17 ++ .../v2/ConsumerAppAccessController.ts | 66 ++++++ .../v2/ConsumerApplicationController.ts | 90 ++++++++ src/controllers/v2/ConsumerPATController.ts | 55 +++++ src/controllers/v2/openapi.yaml | 155 +++++++++++++- src/controllers/v2/routes.ts | 192 +++++++++++++++++- src/services/keystone/batch-service.ts | 1 + src/tsoa-v2.json | 9 + 8 files changed, 571 insertions(+), 14 deletions(-) create mode 100644 src/controllers/v2/ConsumerAppAccessController.ts create mode 100644 src/controllers/v2/ConsumerApplicationController.ts create mode 100644 src/controllers/v2/ConsumerPATController.ts diff --git a/src/auth/auth-tsoa.ts b/src/auth/auth-tsoa.ts index 97ef15c86..bce5d0c68 100644 --- a/src/auth/auth-tsoa.ts +++ b/src/auth/auth-tsoa.ts @@ -39,6 +39,23 @@ export function expressAuthentication( securityName: string, scopes?: string[] ): Promise { + if (securityName === 'pat') { + return new Promise((resolve: any, reject: any) => { + logger.info('Headers %j', Object.keys(request.headers)); + if (request.headers && request.get('X-Consumer-Username')) { + resolve({ + preferred_username: request.get('X-Consumer-Username'), + scope: '', + }); + } else { + reject( + new UnauthorizedError('invalid_token', { + message: 'Denied access to resource', + }) + ); + } + }); + } return new Promise((resolve: any, reject: any) => { verifyJWT(request, null, (err: any) => { if (err) { diff --git a/src/controllers/v2/ConsumerAppAccessController.ts b/src/controllers/v2/ConsumerAppAccessController.ts new file mode 100644 index 000000000..9f038905c --- /dev/null +++ b/src/controllers/v2/ConsumerAppAccessController.ts @@ -0,0 +1,66 @@ +import { + Body, + Controller, + OperationId, + Request, + Put, + Path, + Route, + Security, + Tags, + Get, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; + +import { Application, ServiceAccess } from './types'; +import { getRecords, removeEmpty } from '../../batch/feed-worker'; +import { Logger } from '../../logger'; +import { BatchWhereClause } from '@/services/keystone/batch-service'; + +const logger = Logger('controllers.AppAccess'); + +@injectable() +@Route('/access') +@Tags('Consumers') +export class ApplicationAccessController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Get a list of application Access + * + * @summary List of Access + */ + @Get() + @OperationId('get-application-access') + @Security('jwt') + @Security('pat') + public async list(@Request() request: any): Promise { + // skip access control - since this API will be adding explicit where clause filtering + const ctx = this.keystone.createContext(request, true); + + const where: BatchWhereClause = { + query: '$owner: String', + clause: '{ application: { owner: { username: $owner }}}', + variables: { + owner: ctx['authedItem']['username'], + }, + }; + + const records: ServiceAccess[] = await getRecords( + ctx, + 'ServiceAccess', + 'myServiceAccesses', + [], + where + ); + + logger.info('Service Access %j', records); + + return records.map((o) => removeEmpty(o)); + } +} diff --git a/src/controllers/v2/ConsumerApplicationController.ts b/src/controllers/v2/ConsumerApplicationController.ts new file mode 100644 index 000000000..7bb46758c --- /dev/null +++ b/src/controllers/v2/ConsumerApplicationController.ts @@ -0,0 +1,90 @@ +import { + Body, + Controller, + OperationId, + Request, + Put, + Path, + Route, + Security, + Tags, + Get, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; + +import { Application } from './types'; +import { getRecords, removeEmpty } from '../../batch/feed-worker'; +import { Logger } from '../../logger'; +import { BatchWhereClause } from '@/services/keystone/batch-service'; + +const logger = Logger('controllers.Application'); + +@injectable() +@Route('/applications') +@Tags('Consumers') +export class ApplicationController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Update metadata about a Application + * > `Required Scope:` Application.Manage + * + * @summary Update Application + */ + @Put('/{app}') + @OperationId('put-application') + @Security('jwt') + @Security('pat') + public async put( + @Path() app: string, + @Body() body: Application, + @Request() request: any + ): Promise { + return { + put: true, + app, + body, + }; + } + + /** + * Get a list of your Applications + * + * @summary List of Applications + */ + @Get() + @OperationId('get-applications') + @Security('jwt') + @Security('pat') + public async list(@Request() request: any): Promise { + // skip access control - since this API will be adding explicit where clause filtering + const ctx = this.keystone.createContext(request, true); + + const where: BatchWhereClause = { + query: '$owner: String', + clause: '{ owner: { username: $owner }}', + variables: { + owner: ctx['authedItem']['username'], + }, + }; + + const records: Application[] = await getRecords( + ctx, + 'Application', + 'allApplications', + [], + where + ); + + logger.info('Applications %j', records); + + return records.map((o) => removeEmpty(o)); + } + + // Update application owners +} diff --git a/src/controllers/v2/ConsumerPATController.ts b/src/controllers/v2/ConsumerPATController.ts new file mode 100644 index 000000000..8e534a6ba --- /dev/null +++ b/src/controllers/v2/ConsumerPATController.ts @@ -0,0 +1,55 @@ +import { + Body, + Controller, + OperationId, + Request, + Put, + Path, + Route, + Security, + Tags, + Get, + Post, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { Application } from './types'; +import { Logger } from '../../logger'; +import { replaceApiKey } from '@/services/workflow/kong-api-key-replace'; + +const logger = Logger('controllers.ConsumerPAT'); + +@injectable() +@Route('/personalaccesstokens') +@Tags('Consumers') +export class PATController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Create PAT + * + * @summary Token + */ + @Post() + @OperationId('create-pat') + @Security('jwt') + public async createPersonalAccessToken( + @Body() body: Application, + @Request() request: any + ): Promise { + logger.info('Create Personal Access Token'); + // Create an API Key for the particular user + // /src/services/workflow/kong-api-key-replace.ts + //await replaceApiKey(); + // keyAuthPK: response['id'], + // apiKey: response['key'], + return { + put: true, + body, + }; + } +} diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 06cbe5a6a..49fed6293 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -5,6 +5,52 @@ components: requestBodies: {} responses: {} schemas: + ApplicationRefID: + type: string + GatewayConsumerRefID: + type: string + EnvironmentRefID: + type: string + ServiceAccess: + properties: + name: + type: string + active: + type: string + aclEnabled: + type: string + consumerType: + type: string + application: + $ref: '#/components/schemas/ApplicationRefID' + consumer: + $ref: '#/components/schemas/GatewayConsumerRefID' + productEnvironment: + $ref: '#/components/schemas/EnvironmentRefID' + type: object + additionalProperties: false + UserRefID: + type: string + OrganizationRefID: + type: string + OrganizationUnitRefID: + type: string + Application: + properties: + appId: + type: string + name: + type: string + description: + type: string + owner: + $ref: '#/components/schemas/UserRefID' + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' + type: object + additionalProperties: false BatchResult: properties: status: @@ -71,10 +117,6 @@ components: tags: - tag1 - tag2 - OrganizationRefID: - type: string - OrganizationUnitRefID: - type: string Dataset: properties: extForeignKey: @@ -645,6 +687,10 @@ components: type: openIdConnect description: 'OIDC Login' openIdConnectUrl: 'https://well_known_endpoint' + pat: + type: apiKey + name: pat + in: header info: title: 'APS Directory API' version: 1.1.0 @@ -655,6 +701,104 @@ info: name: 'BC Gov APS' openapi: 3.0.0 paths: + /access: + get: + operationId: get-application-access + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ServiceAccess' + type: array + description: 'Get a list of application Access' + summary: 'List of Access' + tags: + - Consumers + security: + - + jwt: [] + - + pat: [] + parameters: [] + '/applications/{app}': + put: + operationId: put-application + responses: + '200': + description: Ok + content: + application/json: + schema: {} + description: "Update metadata about a Application\n> `Required Scope:` Application.Manage" + summary: 'Update Application' + tags: + - Consumers + security: + - + jwt: [] + - + pat: [] + parameters: + - + in: path + name: app + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + /applications: + get: + operationId: get-applications + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Application' + type: array + description: 'Get a list of your Applications' + summary: 'List of Applications' + tags: + - Consumers + security: + - + jwt: [] + - + pat: [] + parameters: [] + /personalaccesstokens: + post: + operationId: create-pat + responses: + '200': + description: Ok + content: + application/json: + schema: {} + description: 'Create PAT' + summary: Token + tags: + - Consumers + security: + - + jwt: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Application' '/namespaces/{ns}/contents': put: operationId: put-content @@ -1773,6 +1917,9 @@ tags: - name: 'API Directory' description: 'Discover all the great BC Government APIs' + - + name: Consumers + description: 'Manage consumer applications and access' - name: Organizations description: 'Manage organizational access control' diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 46048762e..2c0b36d7a 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -3,6 +3,12 @@ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse } from '@tsoa/runtime'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { ApplicationAccessController } from './ConsumerAppAccessController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { ApplicationController } from './ConsumerApplicationController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { PATController } from './ConsumerPATController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { ContentController } from './ContentController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { OrgDatasetController } from './OrgDatasetController'; @@ -40,6 +46,63 @@ const upload = multer(); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const models: TsoaRoute.Models = { + "ApplicationRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GatewayConsumerRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "EnvironmentRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ServiceAccess": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "active": {"dataType":"string"}, + "aclEnabled": {"dataType":"string"}, + "consumerType": {"dataType":"string"}, + "application": {"ref":"ApplicationRefID"}, + "consumer": {"ref":"GatewayConsumerRefID"}, + "productEnvironment": {"ref":"EnvironmentRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OrganizationRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OrganizationUnitRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Application": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"string"}, + "description": {"dataType":"string"}, + "owner": {"ref":"UserRefID"}, + "organization": {"ref":"OrganizationRefID"}, + "organizationUnit": {"ref":"OrganizationUnitRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "BatchResult": { "dataType": "refObject", "properties": { @@ -73,16 +136,6 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "OrganizationRefID": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "OrganizationUnitRefID": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Dataset": { "dataType": "refObject", "properties": { @@ -410,6 +463,125 @@ export function RegisterRoutes(app: express.Router) { // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa // ########################################################################################################### + app.get('/ds/api/v2/access', + authenticateMiddleware([{"jwt":[]},{"pat":[]}]), + + async function ApplicationAccessController_list(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ApplicationAccessController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.list.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v2/applications/:app', + authenticateMiddleware([{"jwt":[]},{"pat":[]}]), + + async function ApplicationController_put(request: any, response: any, next: any) { + const args = { + app: {"in":"path","name":"app","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Application"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ApplicationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.put.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v2/applications', + authenticateMiddleware([{"jwt":[]},{"pat":[]}]), + + async function ApplicationController_list(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ApplicationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.list.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.post('/ds/api/v2/personalaccesstokens', + authenticateMiddleware([{"jwt":[]}]), + + async function PATController_createPersonalAccessToken(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"Application"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(PATController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.createPersonalAccessToken.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.put('/ds/api/v2/namespaces/:ns/contents', authenticateMiddleware([{"jwt":["Content.Publish"]}]), diff --git a/src/services/keystone/batch-service.ts b/src/services/keystone/batch-service.ts index 3ed6c076e..97a9ac94b 100644 --- a/src/services/keystone/batch-service.ts +++ b/src/services/keystone/batch-service.ts @@ -38,6 +38,7 @@ export class BatchService { }`; } logger.debug('[listAll] %s', queryString); + logger.debug('[listAll] Variables %j', where ? where.variables : {}); const result = await this.context.executeGraphQL({ query: queryString, diff --git a/src/tsoa-v2.json b/src/tsoa-v2.json index 5046af3f3..dc9cc7a97 100644 --- a/src/tsoa-v2.json +++ b/src/tsoa-v2.json @@ -29,6 +29,11 @@ "type": "openIdConnect", "description": "OIDC Login", "openIdConnectUrl": "https://well_known_endpoint" + }, + "pat": { + "type": "apiKey", + "name": "pat", + "in": "header" } }, "tags": [ @@ -36,6 +41,10 @@ "name": "API Directory", "description": "Discover all the great BC Government APIs" }, + { + "name": "Consumers", + "description": "Manage consumer applications and access" + }, { "name": "Organizations", "description": "Manage organizational access control"