From 9c5660944eea03627a82387e923b38fdc9bf3746 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 09:20:51 -0700 Subject: [PATCH 001/109] add new method to login --- src/auth/methods.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/auth/methods.json b/src/auth/methods.json index 774cc93fa..e3c1e2a8b 100644 --- a/src/auth/methods.json +++ b/src/auth/methods.json @@ -17,5 +17,9 @@ "github": { "text": "Github", "description": "" + }, + "verifiedcredential": { + "text": "Verified Credential", + "description": "A digital credential that proves your identity and is issued by a trusted authority." } } From 7fe34e9d69adcb23fa2cf09d288f535682596b36 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 09:27:41 -0700 Subject: [PATCH 002/109] add new method to login --- src/auth/methods.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/methods.json b/src/auth/methods.json index e3c1e2a8b..ccb5f2da4 100644 --- a/src/auth/methods.json +++ b/src/auth/methods.json @@ -18,7 +18,7 @@ "text": "Github", "description": "" }, - "verifiedcredential": { + "digitalcredential": { "text": "Verified Credential", "description": "A digital credential that proves your identity and is issued by a trusted authority." } From 574c065f55fccdf0527798ccb8fdca4f1f61c975 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 10:45:20 -0700 Subject: [PATCH 003/109] upd login method --- src/auth/methods.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/methods.json b/src/auth/methods.json index ccb5f2da4..05d36d24f 100644 --- a/src/auth/methods.json +++ b/src/auth/methods.json @@ -19,7 +19,7 @@ "description": "" }, "digitalcredential": { - "text": "Verified Credential", + "text": "Digital Credential", "description": "A digital credential that proves your identity and is issued by a trusted authority." } } From b3f108be6052e7a21b7d9fef828e7324d95ac098 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 16:22:33 -0700 Subject: [PATCH 004/109] new org controller --- src/controllers/v3/OrganizationController.ts | 49 +++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 1597c3a26..0251bfc5b 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -12,6 +12,8 @@ import { Get, Tags, Post, + FieldErrors, + ValidateError, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; @@ -24,6 +26,7 @@ import { transformAllRefID, syncRecordsThrowErrors, parseBlobString, + replaceKey, } from '../../batch/feed-worker'; import { GroupAccessService, @@ -41,12 +44,13 @@ import { } from '../../services/org-groups/types'; import { getOrganizations, getOrganizationUnit } from '../../services/keystone'; import { getActivity } from '../../services/keystone/activity'; -import { Activity, Organization } from './types'; +import { Activity, Gateway, Organization } from './types'; import { isParent } from '../../services/org-groups/group-converter-utils'; import { ActivitySummary } from '../../services/keystone/types'; import { ActivityDetail } from './types-extra'; import { BatchResult } from '../../batch/types'; import { assertEqual } from '../ioc/assert'; +import { gql } from 'graphql-request'; @injectable() @Route('/organizations') @@ -195,6 +199,40 @@ export class OrganizationController extends Controller { return await svc.listAssignedNamespacesByOrg(org); } + /** + * Create a gateway + * + * @summary Create Gateway + * @param ns + * @param request + * @returns + */ + @Post('{org}/gateways') + @OperationId('create-gateway') + @Security('jwt', ['Namespace.Assign']) + public async create( + @Request() request: any, + @Body() vars: Gateway + ): Promise { + const modifiedVars = replaceKey(vars, 'gatewayId', 'name'); + const result = await this.keystone.executeGraphQL({ + context: this.keystone.createContext(request), + query: createNS, + variables: modifiedVars, + }); + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + throw new ValidateError(errors, 'Unable to create Gateway'); + } + return { + gatewayId: result.data.createNamespace.name, + displayName: result.data.createNamespace.displayName, + }; + } + /** * > `Required Scope:` Gateway.Assign */ @@ -306,3 +344,12 @@ export class OrganizationController extends Controller { .map((o) => parseBlobString(o)); } } + +const createNS = gql` + mutation CreateNamespace($name: String, $displayName: String) { + createNamespace(name: $name, displayName: $displayName) { + name + displayName + } + } +`; \ No newline at end of file From 61db4259eb4644d1a1ead6ebdd28ca6a989c4742 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 16:44:05 -0700 Subject: [PATCH 005/109] add org gateway creation --- src/controllers/v3/OrganizationController.ts | 4 +- src/controllers/v3/openapi.yaml | 1746 +++++++++++++++++ src/controllers/v3/routes.ts | 1756 ++++++++++++++++++ 3 files changed, 3504 insertions(+), 2 deletions(-) create mode 100644 src/controllers/v3/openapi.yaml create mode 100644 src/controllers/v3/routes.ts diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 0251bfc5b..5cedf652f 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -208,9 +208,9 @@ export class OrganizationController extends Controller { * @returns */ @Post('{org}/gateways') - @OperationId('create-gateway') + @OperationId('organization-create-gateway') @Security('jwt', ['Namespace.Assign']) - public async create( + public async createGateway( @Request() request: any, @Body() vars: Gateway ): Promise { diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml new file mode 100644 index 000000000..1d03b29a6 --- /dev/null +++ b/src/controllers/v3/openapi.yaml @@ -0,0 +1,1746 @@ +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + DatasetContact: + properties: + name: + type: string + email: + type: string + role: + type: string + enum: + - pointOfContact + nullable: false + type: object + additionalProperties: false + DatasetResource: + properties: + id: + type: string + name: + type: string + format: + type: string + enum: + - openapi-json + - json + url: + type: string + type: object + additionalProperties: false + OrganizationRefID: + type: string + OrganizationUnitRefID: + type: string + Dataset: + properties: + extForeignKey: + type: string + name: + type: string + license_title: + type: string + security_class: + type: string + view_audience: + type: string + download_audience: + type: string + record_publish_date: + type: string + notes: + type: string + title: + type: string + isInCatalog: + type: string + isDraft: + type: string + contacts: + items: + $ref: '#/components/schemas/DatasetContact' + type: array + resources: + items: + $ref: '#/components/schemas/DatasetResource' + type: array + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' + type: object + additionalProperties: false + BatchResult: + properties: + status: + type: number + format: double + result: + type: string + reason: + type: string + id: + type: string + ownedBy: + type: string + childResults: + items: + $ref: '#/components/schemas/BatchResult' + type: array + required: + - status + - result + type: object + additionalProperties: false + DraftDataset: + properties: + name: + type: string + license_title: + type: string + security_class: + type: string + enum: + - HIGH-CABINET + - HIGH-CONFIDENTIAL + - HIGH-SENSITIVITY + - MEDIUM-SENSITIVITY + - MEDIUM-PERSONAL + - LOW-SENSITIVITY + - LOW-PUBLIC + - PUBLIC + - 'PROTECTED A' + - 'PROTECTED B' + - 'PROTECTED C' + view_audience: + type: string + enum: + - Public + - Government + - 'Named users' + - 'Government and Business BCeID' + download_audience: + type: string + enum: + - Public + - Government + - 'Named users' + - 'Government and Business BCeID' + record_publish_date: + type: string + notes: + type: string + title: + type: string + isInCatalog: + type: boolean + isDraft: + type: boolean + contacts: + items: + $ref: '#/components/schemas/DatasetContact' + type: array + resources: + items: + $ref: '#/components/schemas/DatasetResource' + type: array + tags: + items: + type: string + type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' + type: object + additionalProperties: false + example: + name: my_sample_dataset + license_title: 'Open Government Licence - British Columbia' + security_class: PUBLIC + view_audience: Public + download_audience: Public + record_publish_date: '2017-09-05' + notes: 'Some notes' + title: 'A title about my dataset' + tags: + - tag1 + - tag2 + organization: ministry-of-citizens-services + organizationUnit: databc + Gateway: + properties: + gatewayId: + type: string + displayName: + type: string + type: object + additionalProperties: false + ActivityDetail: + properties: + id: + type: string + message: + type: string + params: + properties: {} + additionalProperties: + type: string + type: object + activityAt: {} + blob: {} + required: + - message + - params + - activityAt + type: object + additionalProperties: false + PublishResult: + properties: + message: + type: string + results: + type: string + error: + type: string + type: object + additionalProperties: false + GatewayServiceRefID: + type: string + GatewayRouteRefID: + type: string + GatewayPlugin: + properties: + extForeignKey: + type: string + name: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + config: {} + service: + $ref: '#/components/schemas/GatewayServiceRefID' + route: + $ref: '#/components/schemas/GatewayRouteRefID' + type: object + additionalProperties: false + GatewayRoute: + properties: + extForeignKey: + type: string + name: + type: string + gatewayId: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + methods: + items: + type: string + type: array + paths: + items: + type: string + type: array + hosts: + items: + type: string + type: array + service: + $ref: '#/components/schemas/GatewayServiceRefID' + plugins: + items: + $ref: '#/components/schemas/GatewayPlugin' + type: array + type: object + additionalProperties: false + IssuerEnvironmentConfig: + properties: + environment: + type: string + exists: + type: boolean + issuerUrl: + type: string + clientRegistration: + type: string + enum: + - anonymous + - managed + - iat + clientId: + type: string + clientSecret: + type: string + initialAccessToken: + type: string + type: object + additionalProperties: false + example: + environment: dev + issuerUrl: 'https://idp.site/auth/realms/my-realm' + clientRegistration: managed + clientId: a-client-id + clientSecret: a-client-secret + undefinedRefID: + type: string + CredentialIssuer: + properties: + name: + type: string + gatewayId: + type: string + description: + type: string + flow: + type: string + enum: + - client-credentials + nullable: false + mode: + type: string + enum: + - auto + nullable: false + authPlugin: + type: string + clientAuthenticator: + type: string + enum: + - client-secret + - client-jwt + - client-jwt-jwks-url + instruction: + type: string + environmentDetails: + items: + $ref: '#/components/schemas/IssuerEnvironmentConfig' + type: array + resourceType: + type: string + resourceAccessScope: + type: string + isShared: + type: boolean + apiKeyName: + type: string + availableScopes: + items: + type: string + type: array + resourceScopes: + items: + type: string + type: array + clientRoles: + items: + type: string + type: array + clientMappers: + items: + type: string + type: array + inheritFrom: + $ref: '#/components/schemas/undefinedRefID' + owner: + $ref: '#/components/schemas/undefinedRefID' + type: object + additionalProperties: false + example: + name: my-auth-profile + description: 'Auth connection to my IdP' + flow: client-credentials + clientAuthenticator: client-secret + mode: auto + environmentDetails: [] + owner: janis@gov.bc.ca + OrganizationUnit: + properties: + extForeignKey: + type: string + name: + type: string + sector: + type: string + title: + type: string + description: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + type: object + additionalProperties: false + Organization: + properties: + extForeignKey: + type: string + name: + type: string + sector: + type: string + title: + type: string + description: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + orgUnits: + items: + $ref: '#/components/schemas/OrganizationUnit' + type: array + type: object + additionalProperties: false + GroupPermission: + properties: + resource: + type: string + scopes: + items: + type: string + type: array + required: + - scopes + type: object + additionalProperties: false + GroupRole: + properties: + name: + type: string + permissions: + items: + $ref: '#/components/schemas/GroupPermission' + type: array + required: + - name + - permissions + type: object + additionalProperties: false + GroupAccess: + properties: + name: + type: string + parent: + type: string + roles: + items: + $ref: '#/components/schemas/GroupRole' + type: array + required: + - roles + type: object + additionalProperties: false + UserReference: + properties: + id: + type: string + email: + type: string + type: object + additionalProperties: false + GroupMember: + properties: + member: + $ref: '#/components/schemas/UserReference' + roles: + items: + type: string + type: array + required: + - member + - roles + type: object + additionalProperties: false + GroupMembership: + properties: + name: + type: string + parent: + type: string + members: + items: + $ref: '#/components/schemas/GroupMember' + type: array + type: object + additionalProperties: false + OrgNamespace: + properties: + name: + type: string + orgUnit: + type: string + enabled: + type: boolean + updatedAt: + type: number + format: double + required: + - name + - orgUnit + - enabled + - updatedAt + type: object + additionalProperties: false + DraftDatasetRefID: + type: string + LegalRefID: + type: string + CredentialIssuerRefID: + type: string + Environment: + properties: + appId: + type: string + name: + type: string + enum: + - dev + - test + - prod + - sandbox + - other + active: + type: boolean + approval: + type: boolean + flow: + type: string + enum: + - public + - protected-externally + - authorization-code + - client-credentials + - kong-acl-only + - kong-api-key-only + - kong-api-key-acl + additionalDetailsToRequest: + type: string + services: + items: + $ref: '#/components/schemas/GatewayServiceRefID' + type: array + legal: + $ref: '#/components/schemas/LegalRefID' + credentialIssuer: + $ref: '#/components/schemas/CredentialIssuerRefID' + type: object + additionalProperties: false + example: + name: dev + active: false + approval: false + flow: public + appId: '00000000' + Product: + properties: + appId: + type: string + name: + type: string + description: + type: string + gatewayId: + type: string + dataset: + $ref: '#/components/schemas/DraftDatasetRefID' + environments: + items: + $ref: '#/components/schemas/Environment' + type: array + type: object + additionalProperties: false + example: + name: my-new-product + appId: '000000000000' + environments: + - + name: dev + active: false + approval: false + flow: public + appId: '00000000' + securitySchemes: + jwt: + type: oauth2 + description: 'Authz Client Credential' + flows: + clientCredentials: + tokenUrl: 'https://token_endpoint' + scopes: {} + portal: + type: http + description: 'Authz Portal Login' + scheme: bearer + bearerFormat: JWT + openid: + type: openIdConnect + description: 'OIDC Login' + openIdConnectUrl: 'https://well_known_endpoint' +info: + title: 'APS Directory API' + version: 3.0.0 + description: 'API Services Portal by BC Gov API Programme Services' + license: + name: MIT + contact: + name: 'BC Gov APS' +openapi: 3.0.0 +paths: + '/organizations/{org}/datasets': + get: + operationId: organization-datasets + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Dataset' + type: array + description: "Get metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Get Organization Datasets' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + put: + operationId: put-organization-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Manage metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Manage Organization Datasets' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DraftDataset' + '/organizations/{org}/datasets/{name}': + delete: + operationId: delete-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Delete a Dataset\n> `Required Scope:` Dataset.Manage" + summary: 'Delete a dataset' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + get: + operationId: get-organization-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + description: "Get metadata about a Dataset that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Get Organization Dataset' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + /directory: + get: + operationId: directory-list + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - 'API Directory' + security: [] + parameters: [] + '/directory/{id}': + get: + operationId: directory-item + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - 'API Directory' + security: [] + parameters: + - + in: path + name: id + required: true + schema: + type: string + '/gateways/{gatewayId}/datasets': + put: + operationId: put-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Update metadata about a Dataset\n> `Required Scope:` Gateway.Manage" + summary: 'Update Dataset' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DraftDataset' + '/gateways/{gatewayId}/datasets/{name}': + get: + operationId: get-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + description: "Get metadata about a Dataset\n> `Required Scope:` Gateway.Manage" + summary: 'Get Dataset' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + /routes/availability: + get: + operationId: check-availability + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - 'Service Routes' + security: [] + parameters: + - + in: query + name: serviceName + required: true + schema: + type: string + - + in: query + name: gatewayId + required: true + schema: + type: string + /gateways/report: + get: + operationId: report + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - Gateways + security: + - + jwt: [] + parameters: + - + in: query + name: ids + required: false + schema: + default: '[]' + type: string + /gateways: + get: + operationId: gateway-list + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Gateway' + type: array + summary: 'List of Gateways available to the user' + tags: + - Gateways + security: + - + jwt: [] + parameters: [] + post: + operationId: create-gateway + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: 'Create a gateway' + summary: 'Create Gateway' + tags: + - Gateways + security: + - + jwt: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + '/gateways/{gatewayId}': + get: + operationId: gateway-profile + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: "Get details about the gateway, such as permissions for what the gateway is setup with.\n> `Required Scope:` Gateway.Manage" + summary: 'Gateway Summary' + tags: + - Gateways + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + delete: + operationId: delete-gateway + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: "Delete the gateway\n> `Required Scope:` Gateway.Manage" + summary: 'Delete Gateway' + tags: + - Gateways + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: query + name: force + required: false + schema: + default: false + type: boolean + '/gateways/{gatewayId}/activity': + get: + operationId: gateway-admin-activity + responses: + '200': + description: 'Activity[]' + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ActivityDetail' + type: array + description: '> `Required Scope:` Gateway.View' + summary: 'Get administration activity for this Gateway' + tags: + - Gateways + security: + - + jwt: + - Namespace.View + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: query + name: first + required: false + schema: + default: 20 + format: double + type: number + - + in: query + name: skip + required: false + schema: + default: 0 + format: double + type: number + '/gateways/{gatewayId}/links': + get: + operationId: get-gateway-links + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + properties: {host: {type: string}} + required: [host] + type: object + type: array + description: "Get a summary of your endpoints\n> `Required Scope:` Gateway.Manage" + summary: 'Get endpoints' + tags: + - Gateways + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/gateways/{gatewayId}/directory/{id}': + get: + operationId: get-ns-directory-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: {} + description: "Used primarily for \"Preview Mode\"\nGet a particular Dataset" + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: id + required: true + schema: + type: string + '/gateways/{gatewayId}/directory': + get: + operationId: get-ns-directory + responses: + '200': + description: Ok + content: + application/json: + schema: {} + description: "Used primarily for \"Preview Mode\"\nList the datasets belonging to a particular Gateway" + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/gateways/{gatewayId}/services': + put: + operationId: publish-gateway-config + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/PublishResult' + tags: + - 'Gateway Services' + security: + - + jwt: + - Gateway.Config + parameters: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + dryRun: + type: string + configFile: + type: string + format: binary + required: + - dryRun + - configFile + get: + operationId: get-gateway-routes + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/GatewayRoute' + type: array + description: "Get a summary of your Gateway Services\n> `Required Scope:` Gateway.Manage" + summary: 'Get Gateway Services' + tags: + - 'Gateway Services' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/identifiers/{type}': + get: + operationId: GetNewID + responses: + '200': + description: Ok + content: + application/json: + schema: + type: string + tags: + - 'New Identifiers' + security: [] + parameters: + - + in: path + name: type + required: true + schema: + type: string + enum: + - environment + - product + - application + - gateway + '/gateways/{gatewayId}/issuers': + put: + operationId: put-issuer + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Create or Update Authorization Profiles\n> `Required Scope:` CredentialIssuer.Admin" + summary: 'Manage Authorization Profiles' + tags: + - 'Authorization Profiles' + security: + - + jwt: + - CredentialIssuer.Admin + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CredentialIssuer' + get: + operationId: get-issuers + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/CredentialIssuer' + type: array + description: "Get Authorization Profiles setup in this Gateway\n> `Required Scope:` Gateway.Manage" + summary: 'Get Authorization Profiles' + tags: + - 'Authorization Profiles' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/gateways/{gatewayId}/issuers/{name}': + delete: + operationId: delete-issuer + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Delete an Authorization Profile\n> `Required Scope:` CredentialIssuer.Admin" + summary: 'Delete Profile' + tags: + - 'Authorization Profiles' + security: + - + jwt: + - CredentialIssuer.Admin + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + /organizations: + get: + operationId: organization-list + responses: + '200': + description: Ok + content: + application/json: + schema: + items: {} + type: array + tags: + - Organizations + security: [] + parameters: [] + '/organizations/{org}': + put: + operationId: put-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Create Organization\n> `Required Scope:` GroupAccess.Manage" + summary: 'Create Organizations' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + get: + operationId: organization-units + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - Organizations + security: [] + parameters: + - + in: path + name: org + required: true + schema: + type: string + '/organizations/{org}/roles': + get: + operationId: get-organization-roles + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupAccess' + description: '> `Required Scope:` GroupAccess.Manage' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + '/organizations/{org}/access': + get: + operationId: get-organization-access + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMembership' + description: '> `Required Scope:` GroupAccess.Manage' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + put: + operationId: put-organization-access + responses: + '204': + description: 'No content' + description: '> `Required Scope:` GroupAccess.Manage' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMembership' + '/organizations/{org}/gateways': + get: + operationId: organization-gateways + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/OrgNamespace' + type: array + description: '> `Required Scope:` Gateway.Assign' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + post: + operationId: organization-create-gateway + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: 'Create a gateway' + summary: 'Create Gateway' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + '/organizations/{org}/{orgUnit}/gateways/{gatewayId}': + put: + operationId: assign-namespace-to-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + result: {type: string} + required: + - result + type: object + description: '> `Required Scope:` Gateway.Assign' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: orgUnit + required: true + schema: + type: string + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: query + name: enable + required: false + schema: + default: true + type: boolean + delete: + operationId: unassign-namespace-from-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + result: {type: string} + required: + - result + type: object + description: '> `Required Scope:` Gateway.Assign' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: orgUnit + required: true + schema: + type: string + - + in: path + name: gatewayId + required: true + schema: + type: string + '/organizations/{org}/activity': + get: + operationId: org-gateway-activity + responses: + '200': + description: 'Activity[]' + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ActivityDetail' + type: array + description: '> `Required Scope:` Gateway.Assign' + summary: 'Get administration activity for Gateways associated with this Organization' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: query + name: first + required: false + schema: + default: 20 + format: double + type: number + - + in: query + name: skip + required: false + schema: + default: 0 + format: double + type: number + /roles: + get: + operationId: GetRoles + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - Organizations + security: [] + parameters: [] + '/gateways/{gatewayId}/products': + put: + operationId: put-product + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Manage Products for APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Manage" + summary: 'Manage Products' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + get: + operationId: get-products + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Product' + type: array + description: "Get Products describing APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Manage" + summary: 'Get Products' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: [] + '/gateways/{gatewayId}/products/{appId}': + delete: + operationId: delete-product + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Delete a Product\n> `Required Scope:` Namespace.Manage" + summary: 'Manage Products' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: appId + required: true + schema: + type: string + '/gateways/{gatewayId}/environments/{appId}': + delete: + operationId: delete-product-environment + responses: + '204': + description: 'No content' + description: "Delete a Product Environment\n> `Required Scope:` Namespace.Manage" + summary: 'Delete a Product Environment' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: appId + required: true + schema: + type: string + - + in: query + name: force + required: false + schema: + default: false + type: boolean +servers: + - + url: /ds/api/v3 +tags: + - + name: 'API Directory' + description: 'Discover all the great BC Government APIs' + - + name: 'API Directory (Administration)' + description: 'Administer datasets on the API Directory' + - + name: Organizations + description: 'Manage organizational access control' + - + name: Gateways + description: 'Get aggregated information about gateways' + - + name: 'Gateway Services' + description: 'View your Gateway Service details' + - + name: Products + description: 'Manage your Products and Environments for publishing to the API Directory' + - + name: 'Authorization Profiles' + description: 'Configure the integration to external Identity Providers' diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts new file mode 100644 index 000000000..b25249139 --- /dev/null +++ b/src/controllers/v3/routes.ts @@ -0,0 +1,1756 @@ +/* tslint:disable */ +/* eslint-disable */ +// 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 { OrgDatasetController } from './OrgDatasetController'; +// 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 { DirectoryController } from './DirectoryController'; +// 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 { DatasetController } from './DatasetController'; +// 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 { EndpointsController } from './EndpointsController'; +// 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 { NamespaceController } from './GatewayController'; +// 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 { GatewayDirectoryController } from './GatewayDirectoryController'; +// 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 { GatewayController } from './GatewayServicesController'; +// 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 { IdentifiersController } from './IdentifierController'; +// 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 { IssuerController } from './IssuerController'; +// 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 { OrganizationController } from './OrganizationController'; +// 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 { OrgRoleController } from './OrgRoleController'; +// 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 { ProductController } from './ProductController'; +import { expressAuthentication } from './../../auth/auth-tsoa'; +// @ts-ignore - no great way to install types from subpackage +const promiseAny = require('promise.any'); +import { iocContainer } from './../ioc'; +import { IocContainer, IocContainerFactory } from '@tsoa/runtime'; +import * as express from 'express'; +const multer = require('multer'); +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 = { + "DatasetContact": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "email": {"dataType":"string"}, + "role": {"dataType":"enum","enums":["pointOfContact"]}, + }, + "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 + "DatasetResource": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "name": {"dataType":"string"}, + "format": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["openapi-json"]},{"dataType":"enum","enums":["json"]}]}, + "url": {"dataType":"string"}, + }, + "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": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "license_title": {"dataType":"string"}, + "security_class": {"dataType":"string"}, + "view_audience": {"dataType":"string"}, + "download_audience": {"dataType":"string"}, + "record_publish_date": {"dataType":"string"}, + "notes": {"dataType":"string"}, + "title": {"dataType":"string"}, + "isInCatalog": {"dataType":"string"}, + "isDraft": {"dataType":"string"}, + "contacts": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetContact"}}, + "resources": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetResource"}}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "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": { + "status": {"dataType":"double","required":true}, + "result": {"dataType":"string","required":true}, + "reason": {"dataType":"string"}, + "id": {"dataType":"string"}, + "ownedBy": {"dataType":"string"}, + "childResults": {"dataType":"array","array":{"dataType":"refObject","ref":"BatchResult"}}, + }, + "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 + "DraftDataset": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "license_title": {"dataType":"string"}, + "security_class": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["HIGH-CABINET"]},{"dataType":"enum","enums":["HIGH-CONFIDENTIAL"]},{"dataType":"enum","enums":["HIGH-SENSITIVITY"]},{"dataType":"enum","enums":["MEDIUM-SENSITIVITY"]},{"dataType":"enum","enums":["MEDIUM-PERSONAL"]},{"dataType":"enum","enums":["LOW-SENSITIVITY"]},{"dataType":"enum","enums":["LOW-PUBLIC"]},{"dataType":"enum","enums":["PUBLIC"]},{"dataType":"enum","enums":["PROTECTED A"]},{"dataType":"enum","enums":["PROTECTED B"]},{"dataType":"enum","enums":["PROTECTED C"]}]}, + "view_audience": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["Public"]},{"dataType":"enum","enums":["Government"]},{"dataType":"enum","enums":["Named users"]},{"dataType":"enum","enums":["Government and Business BCeID"]}]}, + "download_audience": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["Public"]},{"dataType":"enum","enums":["Government"]},{"dataType":"enum","enums":["Named users"]},{"dataType":"enum","enums":["Government and Business BCeID"]}]}, + "record_publish_date": {"dataType":"string"}, + "notes": {"dataType":"string"}, + "title": {"dataType":"string"}, + "isInCatalog": {"dataType":"boolean"}, + "isDraft": {"dataType":"boolean"}, + "contacts": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetContact"}}, + "resources": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetResource"}}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "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 + "Gateway": { + "dataType": "refObject", + "properties": { + "gatewayId": {"dataType":"string"}, + "displayName": {"dataType":"string"}, + }, + "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 + "ActivityDetail": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "message": {"dataType":"string","required":true}, + "params": {"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"string"},"required":true}, + "activityAt": {"dataType":"any","required":true}, + "blob": {"dataType":"any"}, + }, + "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 + "PublishResult": { + "dataType": "refObject", + "properties": { + "message": {"dataType":"string"}, + "results": {"dataType":"string"}, + "error": {"dataType":"string"}, + }, + "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 + "GatewayServiceRefID": { + "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 + "GatewayRouteRefID": { + "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 + "GatewayPlugin": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "config": {"dataType":"any"}, + "service": {"ref":"GatewayServiceRefID"}, + "route": {"ref":"GatewayRouteRefID"}, + }, + "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 + "GatewayRoute": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "methods": {"dataType":"array","array":{"dataType":"string"}}, + "paths": {"dataType":"array","array":{"dataType":"string"}}, + "hosts": {"dataType":"array","array":{"dataType":"string"}}, + "service": {"ref":"GatewayServiceRefID"}, + "plugins": {"dataType":"array","array":{"dataType":"refObject","ref":"GatewayPlugin"}}, + }, + "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 + "IssuerEnvironmentConfig": { + "dataType": "refObject", + "properties": { + "environment": {"dataType":"string"}, + "exists": {"dataType":"boolean"}, + "issuerUrl": {"dataType":"string"}, + "clientRegistration": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["anonymous"]},{"dataType":"enum","enums":["managed"]},{"dataType":"enum","enums":["iat"]}]}, + "clientId": {"dataType":"string"}, + "clientSecret": {"dataType":"string"}, + "initialAccessToken": {"dataType":"string"}, + }, + "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 + "undefinedRefID": { + "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 + "CredentialIssuer": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "description": {"dataType":"string"}, + "flow": {"dataType":"enum","enums":["client-credentials"]}, + "mode": {"dataType":"enum","enums":["auto"]}, + "authPlugin": {"dataType":"string"}, + "clientAuthenticator": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["client-secret"]},{"dataType":"enum","enums":["client-jwt"]},{"dataType":"enum","enums":["client-jwt-jwks-url"]}]}, + "instruction": {"dataType":"string"}, + "environmentDetails": {"dataType":"array","array":{"dataType":"refObject","ref":"IssuerEnvironmentConfig"}}, + "resourceType": {"dataType":"string"}, + "resourceAccessScope": {"dataType":"string"}, + "isShared": {"dataType":"boolean"}, + "apiKeyName": {"dataType":"string"}, + "availableScopes": {"dataType":"array","array":{"dataType":"string"}}, + "resourceScopes": {"dataType":"array","array":{"dataType":"string"}}, + "clientRoles": {"dataType":"array","array":{"dataType":"string"}}, + "clientMappers": {"dataType":"array","array":{"dataType":"string"}}, + "inheritFrom": {"ref":"undefinedRefID"}, + "owner": {"ref":"undefinedRefID"}, + }, + "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 + "OrganizationUnit": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "sector": {"dataType":"string"}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + }, + "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 + "Organization": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "sector": {"dataType":"string"}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "orgUnits": {"dataType":"array","array":{"dataType":"refObject","ref":"OrganizationUnit"}}, + }, + "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 + "GroupPermission": { + "dataType": "refObject", + "properties": { + "resource": {"dataType":"string"}, + "scopes": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "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 + "GroupRole": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "permissions": {"dataType":"array","array":{"dataType":"refObject","ref":"GroupPermission"},"required":true}, + }, + "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 + "GroupAccess": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "parent": {"dataType":"string"}, + "roles": {"dataType":"array","array":{"dataType":"refObject","ref":"GroupRole"},"required":true}, + }, + "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 + "UserReference": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "email": {"dataType":"string"}, + }, + "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 + "GroupMember": { + "dataType": "refObject", + "properties": { + "member": {"ref":"UserReference","required":true}, + "roles": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "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 + "GroupMembership": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "parent": {"dataType":"string"}, + "members": {"dataType":"array","array":{"dataType":"refObject","ref":"GroupMember"}}, + }, + "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 + "OrgNamespace": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "orgUnit": {"dataType":"string","required":true}, + "enabled": {"dataType":"boolean","required":true}, + "updatedAt": {"dataType":"double","required":true}, + }, + "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 + "DraftDatasetRefID": { + "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 + "LegalRefID": { + "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 + "CredentialIssuerRefID": { + "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 + "Environment": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, + "active": {"dataType":"boolean"}, + "approval": {"dataType":"boolean"}, + "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, + "additionalDetailsToRequest": {"dataType":"string"}, + "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, + "legal": {"ref":"LegalRefID"}, + "credentialIssuer": {"ref":"CredentialIssuerRefID"}, + }, + "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 + "Product": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"string"}, + "description": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "dataset": {"ref":"DraftDatasetRefID"}, + "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + }, + "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 +}; +const validationService = new ValidationService(models); + +// 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 + +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/v3/organizations/:org/datasets', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_getDatasets(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + 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(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDatasets.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/v3/organizations/:org/datasets', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_putDataset(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"DraftDataset"}, + 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(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.putDataset.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.delete('/ds/api/v3/organizations/:org/datasets/:name', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_delete(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + 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(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.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/v3/organizations/:org/datasets/:name', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_getDataset(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + 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(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDataset.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/v3/directory', + + async function DirectoryController_list(request: any, response: any, next: any) { + const args = { + }; + + // 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(DirectoryController); + 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.get('/ds/api/v3/directory/:id', + + async function DirectoryController_get(request: any, response: any, next: any) { + const args = { + id: {"in":"path","name":"id","required":true,"dataType":"string"}, + }; + + // 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(DirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.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/v3/gateways/:gatewayId/datasets', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function DatasetController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"DraftDataset"}, + 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(DatasetController); + 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/v3/gateways/:gatewayId/datasets/:name', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function DatasetController_getDataset(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + 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(DatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDataset.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/v3/routes/availability', + + async function EndpointsController_check(request: any, response: any, next: any) { + const args = { + serviceName: {"in":"query","name":"serviceName","required":true,"dataType":"string"}, + gatewayId: {"in":"query","name":"gatewayId","required":true,"dataType":"string"}, + 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(EndpointsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.check.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/v3/gateways/report', + authenticateMiddleware([{"jwt":[]}]), + + async function NamespaceController_report(request: any, response: any, next: any) { + const args = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + ids: {"default":"[]","in":"query","name":"ids","dataType":"string"}, + }; + + // 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(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.report.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/v3/gateways', + authenticateMiddleware([{"jwt":[]}]), + + async function NamespaceController_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(NamespaceController); + 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.get('/ds/api/v3/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceController_profile(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + 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(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.profile.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/v3/gateways', + authenticateMiddleware([{"jwt":[]}]), + + async function NamespaceController_create(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + vars: {"in":"body","name":"vars","required":true,"ref":"Gateway"}, + }; + + // 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(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.create.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.delete('/ds/api/v3/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceController_delete(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + force: {"default":false,"in":"query","name":"force","dataType":"boolean"}, + 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(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.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/v3/gateways/:gatewayId/activity', + authenticateMiddleware([{"jwt":["Namespace.View"]}]), + + async function NamespaceController_namespaceActivity(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + first: {"default":20,"in":"query","name":"first","dataType":"double"}, + skip: {"default":0,"in":"query","name":"skip","dataType":"double"}, + }; + + // 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(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.namespaceActivity.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/v3/gateways/:gatewayId/links', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceController_get(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + 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(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.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/v3/gateways/:gatewayId/directory/:id', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function GatewayDirectoryController_getDataset(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + id: {"in":"path","name":"id","required":true,"dataType":"string"}, + 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(GatewayDirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDataset.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/v3/gateways/:gatewayId/directory', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function GatewayDirectoryController_getDatasets(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + 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(GatewayDirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDatasets.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/v3/gateways/:gatewayId/services', + authenticateMiddleware([{"jwt":["Gateway.Config"]}]), + upload.single('configFile'), + + async function GatewayController_put(request: any, response: any, next: any) { + const args = { + dryRun: {"in":"formData","name":"dryRun","required":true,"dataType":"string"}, + configFile: {"in":"formData","name":"configFile","required":true,"dataType":"file"}, + }; + + // 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(GatewayController); + 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/v3/gateways/:gatewayId/services', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function GatewayController_getServices(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + 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(GatewayController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getServices.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/v3/identifiers/:type', + + async function IdentifiersController_getNewID(request: any, response: any, next: any) { + const args = { + type: {"in":"path","name":"type","required":true,"dataType":"union","subSchemas":[{"dataType":"enum","enums":["environment"]},{"dataType":"enum","enums":["product"]},{"dataType":"enum","enums":["application"]},{"dataType":"enum","enums":["gateway"]}]}, + }; + + // 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(IdentifiersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getNewID.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/v3/gateways/:gatewayId/issuers', + authenticateMiddleware([{"jwt":["CredentialIssuer.Admin"]}]), + + async function IssuerController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"CredentialIssuer"}, + 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(IssuerController); + 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/v3/gateways/:gatewayId/issuers', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function IssuerController_get(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + 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(IssuerController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.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.delete('/ds/api/v3/gateways/:gatewayId/issuers/:name', + authenticateMiddleware([{"jwt":["CredentialIssuer.Admin"]}]), + + async function IssuerController_delete(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + 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(IssuerController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.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/v3/organizations', + + async function OrganizationController_listOrganizations(request: any, response: any, next: any) { + const args = { + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.listOrganizations.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/v3/organizations/:org', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_post(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Organization"}, + 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.post.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/v3/organizations/:org', + + async function OrganizationController_listOrganizationUnits(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.listOrganizationUnits.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/v3/organizations/:org/roles', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_getPolicies(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getPolicies.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/v3/organizations/:org/access', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_get(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.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/v3/organizations/:org/access', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_put(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"GroupMembership"}, + }; + + // 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(OrganizationController); + 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/v3/organizations/:org/gateways', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_listNamespaces(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.listNamespaces.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/v3/organizations/:org/gateways', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_createGateway(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + vars: {"in":"body","name":"vars","required":true,"ref":"Gateway"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.createGateway.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/v3/organizations/:org/:orgUnit/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_assignNamespace(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + orgUnit: {"in":"path","name":"orgUnit","required":true,"dataType":"string"}, + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + enable: {"default":true,"in":"query","name":"enable","dataType":"boolean"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.assignNamespace.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.delete('/ds/api/v3/organizations/:org/:orgUnit/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_unassignNamespace(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + orgUnit: {"in":"path","name":"orgUnit","required":true,"dataType":"string"}, + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.unassignNamespace.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/v3/organizations/:org/activity', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_namespaceActivity(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + first: {"default":20,"in":"query","name":"first","dataType":"double"}, + skip: {"default":0,"in":"query","name":"skip","dataType":"double"}, + }; + + // 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(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.namespaceActivity.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/v3/roles', + + async function OrgRoleController_getRoles(request: any, response: any, next: any) { + const args = { + }; + + // 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(OrgRoleController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getRoles.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/v3/gateways/:gatewayId/products', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Product"}, + 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(ProductController); + 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/v3/gateways/:gatewayId/products', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_get(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(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.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.delete('/ds/api/v3/gateways/:gatewayId/products/:appId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_delete(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + appId: {"in":"path","name":"appId","required":true,"dataType":"string"}, + 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(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.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.delete('/ds/api/v3/gateways/:gatewayId/environments/:appId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_deleteEnvironment(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + appId: {"in":"path","name":"appId","required":true,"dataType":"string"}, + force: {"default":false,"in":"query","name":"force","dataType":"boolean"}, + 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(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.deleteEnvironment.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 + + // 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 + + + // 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 + + function authenticateMiddleware(security: TsoaRoute.Security[] = []) { + return async function runAuthenticationMiddleware(request: any, _response: any, next: any) { + + // 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 + + // keep track of failed auth attempts so we can hand back the most + // recent one. This behavior was previously existing so preserving it + // here + const failedAttempts: any[] = []; + const pushAndRethrow = (error: any) => { + failedAttempts.push(error); + throw error; + }; + + const secMethodOrPromises: Promise[] = []; + for (const secMethod of security) { + if (Object.keys(secMethod).length > 1) { + const secMethodAndPromises: Promise[] = []; + + for (const name in secMethod) { + secMethodAndPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + + // 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 + + secMethodOrPromises.push(Promise.all(secMethodAndPromises) + .then(users => { return users[0]; })); + } else { + for (const name in secMethod) { + secMethodOrPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + } + } + + // 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 + + try { + request['user'] = await promiseAny(secMethodOrPromises); + next(); + } + catch(err) { + // Show most recent error as response + const error = failedAttempts.pop(); + error.status = error.status || 401; + next(error); + } + + // 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 + } + } + + // 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 + + function isController(object: any): object is Controller { + return 'getHeaders' in object && 'getStatus' in object && 'setStatus' in object; + } + + function promiseHandler(controllerObj: any, promise: any, response: any, successStatus: any, next: any) { + return Promise.resolve(promise) + .then((data: any) => { + let statusCode = successStatus; + let headers; + if (isController(controllerObj)) { + headers = controllerObj.getHeaders(); + statusCode = controllerObj.getStatus() || statusCode; + } + + // 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 + + returnHandler(response, statusCode, data, headers) + }) + .catch((error: any) => next(error)); + } + + // 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 + + function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) { + if (response.headersSent) { + return; + } + Object.keys(headers).forEach((name: string) => { + response.set(name, headers[name]); + }); + if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') { + data.pipe(response); + } else if (data !== null && data !== undefined) { + response.status(statusCode || 200).json(data); + } else { + response.status(statusCode || 204).end(); + } + } + + // 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 + + function responder(response: any): TsoaResponse { + return function(status, data, headers) { + returnHandler(response, status, data, headers); + }; + }; + + // 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 + + function getValidatedArgs(args: any, request: any, response: any): any[] { + const fieldErrors: FieldErrors = {}; + const values = Object.keys(args).map((key) => { + const name = args[key].name; + switch (args[key].in) { + case 'request': + return request; + case 'query': + return validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'path': + return validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'header': + return validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'body': + return validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'body-prop': + return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'formData': + if (args[key].dataType === 'file') { + return validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { + return validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } else { + return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } + case 'res': + return responder(response); + } + }); + + if (Object.keys(fieldErrors).length > 0) { + throw new ValidateError(fieldErrors, ''); + } + return values; + } + + // 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 +} + +// 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 From 9fa18626d7dc5b137dd06e8d1108f3f868733e55 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 17:08:50 -0700 Subject: [PATCH 006/109] add missing org --- src/controllers/v3/OrganizationController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 5cedf652f..3244e42b4 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -211,6 +211,7 @@ export class OrganizationController extends Controller { @OperationId('organization-create-gateway') @Security('jwt', ['Namespace.Assign']) public async createGateway( + @Path() org: string, @Request() request: any, @Body() vars: Gateway ): Promise { @@ -229,7 +230,7 @@ export class OrganizationController extends Controller { } return { gatewayId: result.data.createNamespace.name, - displayName: result.data.createNamespace.displayName, + displayName: result.data.createNamespace.displayName }; } From e10631edb73c4b80a88a13406f9a818031b211c4 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 17:23:36 -0700 Subject: [PATCH 007/109] extra parameters for creating a gateway --- src/controllers/v3/OrganizationController.ts | 4 ++-- src/controllers/v3/types.ts | 2 ++ src/lists/extensions/Namespace.ts | 24 ++++++++++++++++++-- src/services/keycloak/group-service.ts | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 3244e42b4..183191733 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -347,8 +347,8 @@ export class OrganizationController extends Controller { } const createNS = gql` - mutation CreateNamespace($name: String, $displayName: String) { - createNamespace(name: $name, displayName: $displayName) { + mutation CreateNamespace($name: String, $displayName: String, $org: String, $domains: String, $dataPlane: String) { + createNamespace(name: $name, displayName: $displayName, org: $org, domains: $domains, dataPlane: $dataPlane) { name displayName } diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index 7157354c0..38001e997 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -145,6 +145,8 @@ export interface Namespace { export interface Gateway { gatewayId?: string; // Primary Key displayName?: string; + domains?: string; + dataPlane?: string; } diff --git a/src/lists/extensions/Namespace.ts b/src/lists/extensions/Namespace.ts index 48041f554..7b19c94aa 100644 --- a/src/lists/extensions/Namespace.ts +++ b/src/lists/extensions/Namespace.ts @@ -460,7 +460,7 @@ module.exports = { }, { schema: - 'createNamespace(name: String, displayName: String): Namespace', + 'createNamespace(name: String, displayName: String, org: String, domains: String, dataPlane: String): Namespace', resolver: async ( item: any, args: any, @@ -554,7 +554,27 @@ module.exports = { envCtx.issuerEnvConfig.clientSecret ); - await kcGroupService.createIfMissing('ns', newNS); + const group = await kcGroupService.createIfMissing('ns', newNS); + + const groupDetail = await kcGroupService.getGroupById(group.id); + + groupDetail.attributes = groupDetail.attributes || {}; + let update = false; + if (args.org) { + update = true; + groupDetail.attributes['org'] = args.org; + } + if (args.domains) { + update = true; + groupDetail.attributes['perm-domains'] = args.domains.split(','); + } + if (args.dataPlane) { + update = true; + groupDetail.attributes['perm-data-plane'] = args.dataPlane; + } + if (update) { + await kcGroupService.updateGroup(groupDetail); + } await recordActivity( context.sudo(), diff --git a/src/services/keycloak/group-service.ts b/src/services/keycloak/group-service.ts index 45a72b7ca..99014aee7 100644 --- a/src/services/keycloak/group-service.ts +++ b/src/services/keycloak/group-service.ts @@ -77,11 +77,11 @@ export class KeycloakGroupService { public async createIfMissing( parentGroupName: string, groupName: string - ): Promise { + ): Promise<{ created: boolean; id: string }> { const groups = (await this.kcAdminClient.groups.find()).filter( (group: GroupRepresentation) => group.name == parentGroupName ); - await this.createIfMissingForParentGroup(groups[0], groupName); + return await this.createIfMissingForParentGroup(groups[0], groupName); } public async createRootGroup(groupName: string) { From 26ecc3548802da46e2698b63e7357f8b7aa0af83 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 17:32:02 -0700 Subject: [PATCH 008/109] org gateway creation can skip access check --- src/controllers/v3/OrganizationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 183191733..9030c30e9 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -217,7 +217,7 @@ export class OrganizationController extends Controller { ): Promise { const modifiedVars = replaceKey(vars, 'gatewayId', 'name'); const result = await this.keystone.executeGraphQL({ - context: this.keystone.createContext(request), + context: this.keystone.createContext(request, true), query: createNS, variables: modifiedVars, }); From 8cc4aaae24226f548be610cadeaeae1cf6d70af0 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 17:34:48 -0700 Subject: [PATCH 009/109] upd v3 org spec --- src/controllers/v3/openapi.yaml | 12 +++++++++++- src/controllers/v3/routes.ts | 3 +++ src/controllers/v3/types.ts | 2 -- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 1d03b29a6..af1ee074c 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -187,6 +187,10 @@ components: type: string displayName: type: string + domains: + type: string + dataPlane: + type: string type: object additionalProperties: false ActivityDetail: @@ -1454,7 +1458,13 @@ paths: - jwt: - Namespace.Assign - parameters: [] + parameters: + - + in: path + name: org + required: true + schema: + type: string requestBody: required: true content: diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index b25249139..301175980 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -134,6 +134,8 @@ const models: TsoaRoute.Models = { "properties": { "gatewayId": {"dataType":"string"}, "displayName": {"dataType":"string"}, + "domains": {"dataType":"string"}, + "dataPlane": {"dataType":"string"}, }, "additionalProperties": false, }, @@ -1331,6 +1333,7 @@ export function RegisterRoutes(app: express.Router) { async function OrganizationController_createGateway(request: any, response: any, next: any) { const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, vars: {"in":"body","name":"vars","required":true,"ref":"Gateway"}, }; diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index 38001e997..7157354c0 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -145,8 +145,6 @@ export interface Namespace { export interface Gateway { gatewayId?: string; // Primary Key displayName?: string; - domains?: string; - dataPlane?: string; } From 6e949c7987fc8d46fed5fac7fde3d6086507eb29 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 17:50:24 -0700 Subject: [PATCH 010/109] fix org gw creation --- src/controllers/v3/OrganizationController.ts | 4 ++-- src/controllers/v3/openapi.yaml | 20 +++++++++++++++----- src/controllers/v3/routes.ts | 16 +++++++++++++--- src/controllers/v3/types-extra.ts | 8 ++++++++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 9030c30e9..a05aaa54e 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -47,7 +47,7 @@ import { getActivity } from '../../services/keystone/activity'; import { Activity, Gateway, Organization } from './types'; import { isParent } from '../../services/org-groups/group-converter-utils'; import { ActivitySummary } from '../../services/keystone/types'; -import { ActivityDetail } from './types-extra'; +import { ActivityDetail, GatewayAdd } from './types-extra'; import { BatchResult } from '../../batch/types'; import { assertEqual } from '../ioc/assert'; import { gql } from 'graphql-request'; @@ -213,7 +213,7 @@ export class OrganizationController extends Controller { public async createGateway( @Path() org: string, @Request() request: any, - @Body() vars: Gateway + @Body() vars: GatewayAdd ): Promise { const modifiedVars = replaceKey(vars, 'gatewayId', 'name'); const result = await this.keystone.executeGraphQL({ diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index af1ee074c..88e84ea3b 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -187,10 +187,6 @@ components: type: string displayName: type: string - domains: - type: string - dataPlane: - type: string type: object additionalProperties: false ActivityDetail: @@ -521,6 +517,20 @@ components: - updatedAt type: object additionalProperties: false + GatewayAdd: + properties: + gatewayId: + type: string + displayName: + type: string + org: + type: string + domains: + type: string + dataPlane: + type: string + type: object + additionalProperties: false DraftDatasetRefID: type: string LegalRefID: @@ -1470,7 +1480,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Gateway' + $ref: '#/components/schemas/GatewayAdd' '/organizations/{org}/{orgUnit}/gateways/{gatewayId}': put: operationId: assign-namespace-to-organization diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 301175980..b7064d4a3 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -134,8 +134,6 @@ const models: TsoaRoute.Models = { "properties": { "gatewayId": {"dataType":"string"}, "displayName": {"dataType":"string"}, - "domains": {"dataType":"string"}, - "dataPlane": {"dataType":"string"}, }, "additionalProperties": false, }, @@ -348,6 +346,18 @@ 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 + "GatewayAdd": { + "dataType": "refObject", + "properties": { + "gatewayId": {"dataType":"string"}, + "displayName": {"dataType":"string"}, + "org": {"dataType":"string"}, + "domains": {"dataType":"string"}, + "dataPlane": {"dataType":"string"}, + }, + "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 "DraftDatasetRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -1335,7 +1345,7 @@ export function RegisterRoutes(app: express.Router) { const args = { org: {"in":"path","name":"org","required":true,"dataType":"string"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, - vars: {"in":"body","name":"vars","required":true,"ref":"Gateway"}, + vars: {"in":"body","name":"vars","required":true,"ref":"GatewayAdd"}, }; // 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 diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 438ddde0d..8ee99974d 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -19,3 +19,11 @@ export interface PublishResult { results?: string; error?: string; } + +export interface GatewayAdd { + gatewayId?: string; // Primary Key + displayName?: string; + org?: string; + domains?: string; + dataPlane?: string; +} \ No newline at end of file From 226fd72b51f528156c6dcf8eca13b2959d904984 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 20:09:56 -0700 Subject: [PATCH 011/109] make each attr an array --- src/lists/extensions/Namespace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lists/extensions/Namespace.ts b/src/lists/extensions/Namespace.ts index 7b19c94aa..c5d64aea1 100644 --- a/src/lists/extensions/Namespace.ts +++ b/src/lists/extensions/Namespace.ts @@ -562,7 +562,7 @@ module.exports = { let update = false; if (args.org) { update = true; - groupDetail.attributes['org'] = args.org; + groupDetail.attributes['org'] = [ args.org ]; } if (args.domains) { update = true; @@ -570,7 +570,7 @@ module.exports = { } if (args.dataPlane) { update = true; - groupDetail.attributes['perm-data-plane'] = args.dataPlane; + groupDetail.attributes['perm-data-plane'] = [ args.dataPlane ]; } if (update) { await kcGroupService.updateGroup(groupDetail); From c43f6b8f7ecc5268e5fd903726ae5a6311820ce7 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 20:42:05 -0700 Subject: [PATCH 012/109] skip org-unit if not present --- src/services/org-groups/namespace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/org-groups/namespace.ts b/src/services/org-groups/namespace.ts index ef321f51f..d60d22a39 100644 --- a/src/services/org-groups/namespace.ts +++ b/src/services/org-groups/namespace.ts @@ -160,7 +160,7 @@ export class NamespaceService { ) .map((group) => ({ name: group.name, - orgUnit: group.attributes['org-unit'][0], + orgUnit: 'org-unit' in group.attributes ? group.attributes['org-unit'][0] : null, enabled: 'org-enabled' in group.attributes ? group.attributes['org-enabled'][0] === 'true' From c11d2123824a251a99ff5f4ebfd99be650dc2d46 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 20:58:27 -0700 Subject: [PATCH 013/109] fix current namespace with org unit --- src/services/keycloak/namespace-details.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/services/keycloak/namespace-details.ts b/src/services/keycloak/namespace-details.ts index 63adf88b6..d7bb99789 100644 --- a/src/services/keycloak/namespace-details.ts +++ b/src/services/keycloak/namespace-details.ts @@ -138,13 +138,17 @@ export async function transformOrgAndOrgUnit( const orgInfo = await getOrganizationUnit(context, merged.orgUnit); if (orgInfo) { merged['org'] = { name: orgInfo.name, title: orgInfo.title }; - merged['orgUnit'] = { - name: orgInfo.orgUnits[0].name, - title: orgInfo.orgUnits[0].title, - }; + if ('orgUnit' in orgInfo) { + merged['orgUnit'] = { + name: orgInfo.orgUnits[0].name, + title: orgInfo.orgUnits[0].title, + }; + } } else { merged['org'] = { name: merged.org, title: merged.org }; - merged['orgUnit'] = { name: merged.orgUnit, title: merged.orgUnit }; + if ('orgUnit' in merged) { + merged['orgUnit'] = { name: merged.orgUnit, title: merged.orgUnit }; + } } // lookup org admins from From 7059944766544bb79619dac9d63bcd720f1522e2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 21:13:48 -0700 Subject: [PATCH 014/109] fix current namespace with org unit --- src/services/keycloak/namespace-details.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/keycloak/namespace-details.ts b/src/services/keycloak/namespace-details.ts index d7bb99789..dc5e4d609 100644 --- a/src/services/keycloak/namespace-details.ts +++ b/src/services/keycloak/namespace-details.ts @@ -138,7 +138,7 @@ export async function transformOrgAndOrgUnit( const orgInfo = await getOrganizationUnit(context, merged.orgUnit); if (orgInfo) { merged['org'] = { name: orgInfo.name, title: orgInfo.title }; - if ('orgUnit' in orgInfo) { + if (orgInfo.orgUnits) { merged['orgUnit'] = { name: orgInfo.orgUnits[0].name, title: orgInfo.orgUnits[0].title, @@ -146,7 +146,7 @@ export async function transformOrgAndOrgUnit( } } else { merged['org'] = { name: merged.org, title: merged.org }; - if ('orgUnit' in merged) { + if (merged.orgUnits) { merged['orgUnit'] = { name: merged.orgUnit, title: merged.orgUnit }; } } From 7f434b41560e10f0f6d43e25b158b2aad74ee781 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 19 Aug 2025 23:07:38 -0700 Subject: [PATCH 015/109] dont skip the auth check --- src/controllers/v3/OrganizationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index a05aaa54e..3399913bc 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -217,7 +217,7 @@ export class OrganizationController extends Controller { ): Promise { const modifiedVars = replaceKey(vars, 'gatewayId', 'name'); const result = await this.keystone.executeGraphQL({ - context: this.keystone.createContext(request, true), + context: this.keystone.createContext(request), query: createNS, variables: modifiedVars, }); From 9b6be9749197d25da4984e1454683ab500b9ec59 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 10:56:20 -0700 Subject: [PATCH 016/109] list updates for sdx --- src/batch/data-rules.js | 29 +++++- src/controllers/v2/openapi.yaml | 13 +++ src/controllers/v2/routes.ts | 7 +- src/controllers/v2/types.ts | 8 +- src/controllers/v3/OrgProductController.ts | 107 +++++++++++++++++++++ src/controllers/v3/openapi.yaml | 70 ++++++++++++++ src/controllers/v3/routes.ts | 70 +++++++++++++- src/controllers/v3/types.ts | 8 +- src/lists/Application.js | 5 + src/lists/Blob.js | 4 + src/lists/CredentialIssuer.js | 5 + src/lists/Environment.js | 1 + src/lists/Product.js | 23 ++++- 13 files changed, 340 insertions(+), 10 deletions(-) create mode 100644 src/controllers/v3/OrgProductController.ts diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 7d91c6274..ed2cbc4a8 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -387,7 +387,7 @@ const metadata = { Application: { query: 'allApplications', refKey: 'appId', - sync: ['name', 'description'], + sync: ['name', 'description', 'namespace'], transformations: { owner: { name: 'connectOne', list: 'allUsers', refKey: 'username' }, organization: { @@ -408,15 +408,29 @@ const metadata = { query: 'allProducts', refKey: 'appId', compositeRefKey: ['name', 'namespace'], - sync: ['name', 'description', 'namespace'], + sync: ['name', 'description', 'namespace', 'organization', 'organizationUnit', 'openapSpecs'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, + openapiSpecs: { name: "toStringDefaultArray" }, environments: { name: 'connectExclusiveListCreate', list: 'Environment', syncFirst: true, refKey: 'appId', }, + organization: { + name: 'connectOne', + key: 'organization.id', + list: 'allOrganizations', + refKey: 'orgUnits.extForeignKey', + }, + organizationUnit: { + name: 'connectOne', + key: 'organization.id', + list: 'allOrganizationUnits', + refKey: 'extForeignKey', + }, + }, example: { name: 'my-new-product', @@ -439,7 +453,7 @@ const metadata = { 'name', { key: 'parent.id', whereClause: 'product: { id: $parent_id }' }, ], - sync: ['name', 'active', 'approval', 'flow', 'additionalDetailsToRequest'], + sync: ['name', 'active', 'approval', 'flow', 'additionalDetailsToRequest', 'spec'], ownedBy: 'product', transformations: { services: { @@ -449,12 +463,19 @@ const metadata = { filterByNamespace: true, }, legal: { name: 'connectOne', list: 'allLegals', refKey: 'reference' }, + spec: { name: 'connectOne', list: 'allBlobs', refKey: 'name' }, credentialIssuer: { name: 'connectOne', list: 'allCredentialIssuers', refKey: 'name', filterByNamespace: true, }, + spec: { + name: 'connectExclusiveOne', + list: 'Blob', + refKey: 'ref', + filterByNamespace: true, + }, }, validations: { active: { type: 'boolean' }, @@ -530,7 +551,7 @@ const metadata = { mode: { type: 'enum', values: ['auto'] }, clientAuthenticator: { type: 'enum', - values: ['client-secret', 'client-jwt', 'client-jwt-jwks-url'], + values: ['client-secret', 'client-jwt', 'client-jwt-jwks-url', 'client-certificate'], }, environmentDetails: { type: 'entityArray', diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 50002bffb..2c6160464 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -352,6 +352,7 @@ components: - client-secret - client-jwt - client-jwt-jwks-url + - client-certificate instruction: type: string environmentDetails: @@ -623,6 +624,8 @@ components: - kong-api-key-acl additionalDetailsToRequest: type: string + spec: + type: string services: items: $ref: '#/components/schemas/GatewayServiceRefID' @@ -649,12 +652,22 @@ components: type: string namespace: type: string + openapSpecs: + type: string + openapiSpecs: + items: + type: string + type: array dataset: $ref: '#/components/schemas/DraftDatasetRefID' environments: items: $ref: '#/components/schemas/Environment' type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' type: object additionalProperties: false example: diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index d141d7077..be221d0b5 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -232,7 +232,7 @@ const models: TsoaRoute.Models = { "flow": {"dataType":"enum","enums":["client-credentials"]}, "mode": {"dataType":"enum","enums":["auto"]}, "authPlugin": {"dataType":"string"}, - "clientAuthenticator": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["client-secret"]},{"dataType":"enum","enums":["client-jwt"]},{"dataType":"enum","enums":["client-jwt-jwks-url"]}]}, + "clientAuthenticator": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["client-secret"]},{"dataType":"enum","enums":["client-jwt"]},{"dataType":"enum","enums":["client-jwt-jwks-url"]},{"dataType":"enum","enums":["client-certificate"]}]}, "instruction": {"dataType":"string"}, "environmentDetails": {"dataType":"array","array":{"dataType":"refObject","ref":"IssuerEnvironmentConfig"}}, "resourceType": {"dataType":"string"}, @@ -402,6 +402,7 @@ const models: TsoaRoute.Models = { "approval": {"dataType":"boolean"}, "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, "additionalDetailsToRequest": {"dataType":"string"}, + "spec": {"dataType":"string"}, "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, "legal": {"ref":"LegalRefID"}, "credentialIssuer": {"ref":"CredentialIssuerRefID"}, @@ -416,8 +417,12 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string"}, "description": {"dataType":"string"}, "namespace": {"dataType":"string"}, + "openapSpecs": {"dataType":"string"}, + "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + "organization": {"ref":"OrganizationRefID"}, + "organizationUnit": {"ref":"OrganizationUnitRefID"}, }, "additionalProperties": false, }, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index c5909c0cc..cdf400632 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -263,6 +263,7 @@ export interface Application { appId?: string; // Primary Key name?: string; description?: string; + namespace?: string; owner?: UserRefID; organization?: OrganizationRefID; organizationUnit?: OrganizationUnitRefID; @@ -290,8 +291,12 @@ export interface Product { name?: string; description?: string; namespace?: string; + openapSpecs?: string; + openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; + organization?: OrganizationRefID; + organizationUnit?: OrganizationUnitRefID; } @@ -312,6 +317,7 @@ export interface Environment { approval?: boolean; flow?: "public" | "protected-externally" | "authorization-code" | "client-credentials" | "kong-acl-only" | "kong-api-key-only" | "kong-api-key-acl"; additionalDetailsToRequest?: string; + spec?: string; services?: GatewayServiceRefID[]; legal?: LegalRefID; credentialIssuer?: CredentialIssuerRefID; @@ -337,7 +343,7 @@ export interface CredentialIssuer { flow?: "client-credentials"; mode?: "auto"; authPlugin?: string; - clientAuthenticator?: "client-secret" | "client-jwt" | "client-jwt-jwks-url"; + clientAuthenticator?: "client-secret" | "client-jwt" | "client-jwt-jwks-url" | "client-certificate"; instruction?: string; environmentDetails?: IssuerEnvironmentConfig[]; resourceType?: string; diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts new file mode 100644 index 000000000..78d18d692 --- /dev/null +++ b/src/controllers/v3/OrgProductController.ts @@ -0,0 +1,107 @@ +import { + Controller, + Request, + OperationId, + Put, + Path, + Route, + Security, + Body, + Get, + Tags, + Delete, +} from 'tsoa'; +import { strict as assert } from 'assert'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecordsThrowErrors, + getRecords, + parseJsonString, + removeEmpty, + removeKeys, + transformAllRefID, + deleteRecord, + replaceKey, +} from '../../batch/feed-worker'; +import { BatchResult } from '../../batch/types'; +import { Dataset, DraftDataset } from './types'; +import { Product } from './types'; + +@injectable() +@Route('/organizations') +@Tags('API Directory (Administration)') +export class OrgProductController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Get metadata about Datasets that are available by API for this organization + * > `Required Scope:` Dataset.Manage + * + * @summary Get Organization Datasets + */ + @Get('/{org}/products') + @OperationId('organization-products') + @Security('jwt', ['Dataset.Manage']) + public async getProducts( + @Path() org: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + + const batchClause = { + query: '$org: String', + clause: '{ organization: { name: $org } }', + variables: { org }, + }; + + const records = await getRecords( + ctx, + 'Product', + undefined, + [], + batchClause + ); + + return records + .map((o) => removeEmpty(o)) + .map((o) => transformAllRefID(o, ['organization', 'organizationUnit'])) + .map((o) => + removeKeys(o, [ + 'id' + ]) + ); + } + + + /** + * Manage Products for APIs that will appear on the API Directory + * > `Required Scope:` Namespace.Manage + * + * @summary Manage Products + * @param ns + * @param body + * @param request + */ + @Put('/{org}/products') + @OperationId('organization-put-product') + @Security('jwt', ['Dataset.Manage']) + public async put( + @Path() org: string, + @Body() body: Product, + @Request() request: any + ): Promise { + // TODO: Make sure namespace is allowed for this org + + return await syncRecordsThrowErrors( + this.keystone.createContext(request), + 'Product', + body['appId'], + body + ); + } +} diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 88e84ea3b..cfd996749 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -335,6 +335,7 @@ components: - client-secret - client-jwt - client-jwt-jwks-url + - client-certificate instruction: type: string environmentDetails: @@ -565,6 +566,8 @@ components: - kong-api-key-acl additionalDetailsToRequest: type: string + spec: + type: string services: items: $ref: '#/components/schemas/GatewayServiceRefID' @@ -591,12 +594,22 @@ components: type: string gatewayId: type: string + openapSpecs: + type: string + openapiSpecs: + items: + type: string + type: array dataset: $ref: '#/components/schemas/DraftDatasetRefID' environments: items: $ref: '#/components/schemas/Environment' type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' type: object additionalProperties: false example: @@ -1610,6 +1623,63 @@ paths: default: 0 format: double type: number + '/organizations/{org}/products': + get: + operationId: organization-products + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Dataset' + type: array + description: "Get metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Get Organization Datasets' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + put: + operationId: organization-put-product + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Manage Products for APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Manage" + summary: 'Manage Products' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Product' /roles: get: operationId: GetRoles diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index b7064d4a3..40e81db87 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -23,6 +23,8 @@ import { IssuerController } from './IssuerController'; // 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 { OrganizationController } from './OrganizationController'; // 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 { OrgProductController } from './OrgProductController'; +// 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 { OrgRoleController } from './OrgRoleController'; // 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 { ProductController } from './ProductController'; @@ -231,7 +233,7 @@ const models: TsoaRoute.Models = { "flow": {"dataType":"enum","enums":["client-credentials"]}, "mode": {"dataType":"enum","enums":["auto"]}, "authPlugin": {"dataType":"string"}, - "clientAuthenticator": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["client-secret"]},{"dataType":"enum","enums":["client-jwt"]},{"dataType":"enum","enums":["client-jwt-jwks-url"]}]}, + "clientAuthenticator": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["client-secret"]},{"dataType":"enum","enums":["client-jwt"]},{"dataType":"enum","enums":["client-jwt-jwks-url"]},{"dataType":"enum","enums":["client-certificate"]}]}, "instruction": {"dataType":"string"}, "environmentDetails": {"dataType":"array","array":{"dataType":"refObject","ref":"IssuerEnvironmentConfig"}}, "resourceType": {"dataType":"string"}, @@ -382,6 +384,7 @@ const models: TsoaRoute.Models = { "approval": {"dataType":"boolean"}, "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, "additionalDetailsToRequest": {"dataType":"string"}, + "spec": {"dataType":"string"}, "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, "legal": {"ref":"LegalRefID"}, "credentialIssuer": {"ref":"CredentialIssuerRefID"}, @@ -396,8 +399,12 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string"}, "description": {"dataType":"string"}, "gatewayId": {"dataType":"string"}, + "openapSpecs": {"dataType":"string"}, + "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + "organization": {"ref":"OrganizationRefID"}, + "organizationUnit": {"ref":"OrganizationUnitRefID"}, }, "additionalProperties": false, }, @@ -1463,6 +1470,67 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/:org/products', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgProductController_getProducts(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + 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(OrgProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getProducts.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/v3/organizations/:org/products', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgProductController_put(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Product"}, + 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(OrgProductController); + 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/v3/roles', async function OrgRoleController_getRoles(request: any, response: any, next: any) { diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index 7157354c0..a9798f825 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -263,6 +263,7 @@ export interface Application { appId?: string; // Primary Key name?: string; description?: string; + gatewayId?: string; owner?: UserRefID; organization?: OrganizationRefID; organizationUnit?: OrganizationUnitRefID; @@ -290,8 +291,12 @@ export interface Product { name?: string; description?: string; gatewayId?: string; + openapSpecs?: string; + openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; + organization?: OrganizationRefID; + organizationUnit?: OrganizationUnitRefID; } @@ -312,6 +317,7 @@ export interface Environment { approval?: boolean; flow?: "public" | "protected-externally" | "authorization-code" | "client-credentials" | "kong-acl-only" | "kong-api-key-only" | "kong-api-key-acl"; additionalDetailsToRequest?: string; + spec?: string; services?: GatewayServiceRefID[]; legal?: LegalRefID; credentialIssuer?: CredentialIssuerRefID; @@ -337,7 +343,7 @@ export interface CredentialIssuer { flow?: "client-credentials"; mode?: "auto"; authPlugin?: string; - clientAuthenticator?: "client-secret" | "client-jwt" | "client-jwt-jwks-url"; + clientAuthenticator?: "client-secret" | "client-jwt" | "client-jwt-jwks-url" | "client-certificate"; instruction?: string; environmentDetails?: IssuerEnvironmentConfig[]; resourceType?: string; diff --git a/src/lists/Application.js b/src/lists/Application.js index dd00f66a5..654e647f0 100644 --- a/src/lists/Application.js +++ b/src/lists/Application.js @@ -25,6 +25,11 @@ module.exports = { type: Text, isRequired: true, }, + namespace: { + type: Text, + isRequired: false, + access: { update: false }, + }, description: { type: Text, isRequired: true, diff --git a/src/lists/Blob.js b/src/lists/Blob.js index 5adf60281..adb1ed9b6 100644 --- a/src/lists/Blob.js +++ b/src/lists/Blob.js @@ -18,6 +18,10 @@ module.exports = { { value: 'json', label: 'JSON' }, ], }, + namespace: { + type: Text, + isRequired: false, + }, blob: { type: Text, isRequired: true, diff --git a/src/lists/CredentialIssuer.js b/src/lists/CredentialIssuer.js index 8f62cce4e..9cddcc2c2 100644 --- a/src/lists/CredentialIssuer.js +++ b/src/lists/CredentialIssuer.js @@ -94,8 +94,13 @@ module.exports = { { value: 'client-secret', label: 'Client ID and Secret' }, { value: 'client-jwt', label: 'Signed JWT' }, { value: 'client-jwt-jwks-url', label: 'Signed JWT with JWKS URL' }, + { value: 'client-certificate', label: 'Client Certificate' }, ], }, + sdxIdentifier: { + type: Text, + isRequired: false, + }, clientMappers: { type: Text, isRequired: false, diff --git a/src/lists/Environment.js b/src/lists/Environment.js index 7f2f4052a..4ac1c7fca 100644 --- a/src/lists/Environment.js +++ b/src/lists/Environment.js @@ -87,6 +87,7 @@ module.exports = { many: false, access: { update: false }, }, + spec: { type: Relationship, ref: 'Blob', many: false, required: false }, }, access: EnforcementPoint, hooks: { diff --git a/src/lists/Product.js b/src/lists/Product.js index 6719819c5..a97e196a5 100644 --- a/src/lists/Product.js +++ b/src/lists/Product.js @@ -1,4 +1,4 @@ -const { Text, Relationship } = require('@keystonejs/fields'); +const { Text, Select, Relationship } = require('@keystonejs/fields'); const { Markdown } = require('@keystonejs/fields-markdown'); const { newProductID, isProductID } = require('../services/identifiers'); const { @@ -19,12 +19,26 @@ module.exports = { appId: { type: Text, isRequired: true, - isUnique: false, + isUnique: true, + access: { + create: true, + update: false, + }, }, name: { type: Text, isRequired: true, }, + type: { + type: Select, + emptyOption: false, + dataType: 'string', + defaultValue: 'service', + options: [ + { value: 'service', label: 'Service' }, + { value: 'app', label: 'Application' }, + ] + }, namespace: { type: Text, isRequired: true, @@ -35,6 +49,11 @@ module.exports = { isMultiline: true, isRequired: false, }, + // JSON structure: {label: '', version: '', blob: 'blod-reference'} + openapiSpecs: { + type: Text, + isRequired: false, + }, dataset: { type: Relationship, ref: 'Dataset' }, organization: { type: Relationship, ref: 'Organization', many: false }, organizationUnit: { type: Relationship, ref: 'OrganizationUnit' }, From 0426a555df0cd4a17970b3eaebb3e603330b6f4c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 11:46:19 -0700 Subject: [PATCH 017/109] upd product controller --- src/batch/data-rules.js | 9 +++------ src/controllers/v3/OrgProductController.ts | 6 ++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index ed2cbc4a8..53f469b1d 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -408,7 +408,7 @@ const metadata = { query: 'allProducts', refKey: 'appId', compositeRefKey: ['name', 'namespace'], - sync: ['name', 'description', 'namespace', 'organization', 'organizationUnit', 'openapSpecs'], + sync: ['name', 'description', 'namespace', 'organization', 'organizationUnit', 'openapiSpecs'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, openapiSpecs: { name: "toStringDefaultArray" }, @@ -420,17 +420,14 @@ const metadata = { }, organization: { name: 'connectOne', - key: 'organization.id', list: 'allOrganizations', - refKey: 'orgUnits.extForeignKey', + refKey: 'name', }, organizationUnit: { name: 'connectOne', - key: 'organization.id', list: 'allOrganizationUnits', - refKey: 'extForeignKey', + refKey: 'name', }, - }, example: { name: 'my-new-product', diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 78d18d692..1392e8fc0 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -87,21 +87,23 @@ export class OrgProductController extends Controller { * @param body * @param request */ - @Put('/{org}/products') + @Put('/{org}/gateways/{gatewayId}/products') @OperationId('organization-put-product') @Security('jwt', ['Dataset.Manage']) public async put( + @Path() gatewayId: string, @Path() org: string, @Body() body: Product, @Request() request: any ): Promise { // TODO: Make sure namespace is allowed for this org + body['gatewayId'] = gatewayId; return await syncRecordsThrowErrors( this.keystone.createContext(request), 'Product', body['appId'], - body + replaceKey(body, 'gatewayId', 'namespace') ); } } From 16d62f30d68b7b1eb9f469b8fa0d977b941be941 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 11:46:56 -0700 Subject: [PATCH 018/109] upd product controller --- src/controllers/v2/openapi.yaml | 2 -- src/controllers/v2/routes.ts | 1 - src/controllers/v2/types.ts | 1 - src/controllers/v3/openapi.yaml | 9 +++++++-- src/controllers/v3/routes.ts | 4 ++-- src/controllers/v3/types.ts | 1 - 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 2c6160464..e5f850e78 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -652,8 +652,6 @@ components: type: string namespace: type: string - openapSpecs: - type: string openapiSpecs: items: type: string diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index be221d0b5..3868c9cf4 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -417,7 +417,6 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string"}, "description": {"dataType":"string"}, "namespace": {"dataType":"string"}, - "openapSpecs": {"dataType":"string"}, "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index cdf400632..7adf8ad59 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -291,7 +291,6 @@ export interface Product { name?: string; description?: string; namespace?: string; - openapSpecs?: string; openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index cfd996749..450e0a8c0 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -594,8 +594,6 @@ components: type: string gatewayId: type: string - openapSpecs: - type: string openapiSpecs: items: type: string @@ -1650,6 +1648,7 @@ paths: required: true schema: type: string + '/organizations/{org}/gateways/{gatewayId}/products': put: operationId: organization-put-product responses: @@ -1668,6 +1667,12 @@ paths: jwt: - Dataset.Manage parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string - in: path name: org diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 40e81db87..f3b903e72 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -399,7 +399,6 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string"}, "description": {"dataType":"string"}, "gatewayId": {"dataType":"string"}, - "openapSpecs": {"dataType":"string"}, "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, @@ -1500,11 +1499,12 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/:org/products', + app.put('/ds/api/v3/organizations/:org/gateways/:gatewayId/products', authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), async function OrgProductController_put(request: any, response: any, next: any) { const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, org: {"in":"path","name":"org","required":true,"dataType":"string"}, body: {"in":"body","name":"body","required":true,"ref":"Product"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index a9798f825..de858dfe4 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -291,7 +291,6 @@ export interface Product { name?: string; description?: string; gatewayId?: string; - openapSpecs?: string; openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; From 647d65fe1674c53dc36f885c88fc1af102f576f2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 12:23:52 -0700 Subject: [PATCH 019/109] fix product put --- src/controllers/v3/OrgProductController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 1392e8fc0..817c66518 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -98,9 +98,10 @@ export class OrgProductController extends Controller { ): Promise { // TODO: Make sure namespace is allowed for this org body['gatewayId'] = gatewayId; + body['organization'] = org; return await syncRecordsThrowErrors( - this.keystone.createContext(request), + this.keystone.createContext(request, true), 'Product', body['appId'], replaceKey(body, 'gatewayId', 'namespace') From 6d2dd03992a7c1ab58bef193437d26c7c1249887 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 12:41:14 -0700 Subject: [PATCH 020/109] upd product details --- src/controllers/v2/openapi.yaml | 6 ++++++ src/controllers/v2/routes.ts | 1 + src/controllers/v2/types.ts | 2 ++ src/controllers/v3/openapi.yaml | 6 ++++++ src/controllers/v3/routes.ts | 1 + src/controllers/v3/types.ts | 2 ++ 6 files changed, 18 insertions(+) diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index e5f850e78..1cd9718d6 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -648,6 +648,11 @@ components: type: string name: type: string + type: + type: string + enum: + - app + - service description: type: string namespace: @@ -671,6 +676,7 @@ components: example: name: my-new-product appId: '000000000000' + type: service environments: - name: dev diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 3868c9cf4..b832312e4 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -415,6 +415,7 @@ const models: TsoaRoute.Models = { "properties": { "appId": {"dataType":"string"}, "name": {"dataType":"string"}, + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["app"]},{"dataType":"enum","enums":["service"]}]}, "description": {"dataType":"string"}, "namespace": {"dataType":"string"}, "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index 7adf8ad59..7e353d343 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -275,6 +275,7 @@ export interface Application { * @example { * "name": "my-new-product", * "appId": "000000000000", + * "type": "service", * "environments": [ * { * "name": "dev", @@ -289,6 +290,7 @@ export interface Application { export interface Product { appId?: string; // Primary Key name?: string; + type?: "app" | "service"; description?: string; namespace?: string; openapiSpecs?: string[]; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 450e0a8c0..46de68b73 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -590,6 +590,11 @@ components: type: string name: type: string + type: + type: string + enum: + - app + - service description: type: string gatewayId: @@ -613,6 +618,7 @@ components: example: name: my-new-product appId: '000000000000' + type: service environments: - name: dev diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index f3b903e72..4dd115728 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -397,6 +397,7 @@ const models: TsoaRoute.Models = { "properties": { "appId": {"dataType":"string"}, "name": {"dataType":"string"}, + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["app"]},{"dataType":"enum","enums":["service"]}]}, "description": {"dataType":"string"}, "gatewayId": {"dataType":"string"}, "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index de858dfe4..36d2f24ea 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -275,6 +275,7 @@ export interface Application { * @example { * "name": "my-new-product", * "appId": "000000000000", + * "type": "service", * "environments": [ * { * "name": "dev", @@ -289,6 +290,7 @@ export interface Application { export interface Product { appId?: string; // Primary Key name?: string; + type?: "app" | "service"; description?: string; gatewayId?: string; openapiSpecs?: string[]; From 2777e187521acddeb4bba1be09c0fa2eed21a08a Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 13:00:53 -0700 Subject: [PATCH 021/109] add type to product --- src/batch/data-rules.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 53f469b1d..cc63947ab 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -408,7 +408,7 @@ const metadata = { query: 'allProducts', refKey: 'appId', compositeRefKey: ['name', 'namespace'], - sync: ['name', 'description', 'namespace', 'organization', 'organizationUnit', 'openapiSpecs'], + sync: ['name', 'type', 'description', 'namespace', 'organization', 'organizationUnit', 'openapiSpecs'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, openapiSpecs: { name: "toStringDefaultArray" }, @@ -429,9 +429,16 @@ const metadata = { refKey: 'name', }, }, + validations: { + type: { + type: 'enum', + values: ['app', 'service'], + }, + }, example: { name: 'my-new-product', appId: '000000000000', + type: 'service', environments: [ { name: 'dev', From 602aeba9fbf9913e769eb8c10442466e86af1d75 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 13:31:37 -0700 Subject: [PATCH 022/109] upd product --- src/batch/data-rules.js | 14 ++------------ src/controllers/v2/openapi.yaml | 4 ---- src/controllers/v2/routes.ts | 2 -- src/controllers/v2/types.ts | 2 -- src/controllers/v3/OrgProductController.ts | 1 - src/controllers/v3/openapi.yaml | 4 ---- src/controllers/v3/routes.ts | 2 -- src/controllers/v3/types.ts | 2 -- 8 files changed, 2 insertions(+), 29 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index cc63947ab..f1d994793 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -408,7 +408,7 @@ const metadata = { query: 'allProducts', refKey: 'appId', compositeRefKey: ['name', 'namespace'], - sync: ['name', 'type', 'description', 'namespace', 'organization', 'organizationUnit', 'openapiSpecs'], + sync: ['name', 'type', 'description', 'namespace', 'openapiSpecs'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, openapiSpecs: { name: "toStringDefaultArray" }, @@ -417,17 +417,7 @@ const metadata = { list: 'Environment', syncFirst: true, refKey: 'appId', - }, - organization: { - name: 'connectOne', - list: 'allOrganizations', - refKey: 'name', - }, - organizationUnit: { - name: 'connectOne', - list: 'allOrganizationUnits', - refKey: 'name', - }, + } }, validations: { type: { diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 1cd9718d6..2251a9256 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -667,10 +667,6 @@ components: items: $ref: '#/components/schemas/Environment' type: array - organization: - $ref: '#/components/schemas/OrganizationRefID' - organizationUnit: - $ref: '#/components/schemas/OrganizationUnitRefID' type: object additionalProperties: false example: diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index b832312e4..8c06c1a51 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -421,8 +421,6 @@ const models: TsoaRoute.Models = { "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, - "organization": {"ref":"OrganizationRefID"}, - "organizationUnit": {"ref":"OrganizationUnitRefID"}, }, "additionalProperties": false, }, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index 7e353d343..72677bc4a 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -296,8 +296,6 @@ export interface Product { openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; - organization?: OrganizationRefID; - organizationUnit?: OrganizationUnitRefID; } diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 817c66518..7d5009a11 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -98,7 +98,6 @@ export class OrgProductController extends Controller { ): Promise { // TODO: Make sure namespace is allowed for this org body['gatewayId'] = gatewayId; - body['organization'] = org; return await syncRecordsThrowErrors( this.keystone.createContext(request, true), diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 46de68b73..07f64e999 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -609,10 +609,6 @@ components: items: $ref: '#/components/schemas/Environment' type: array - organization: - $ref: '#/components/schemas/OrganizationRefID' - organizationUnit: - $ref: '#/components/schemas/OrganizationUnitRefID' type: object additionalProperties: false example: diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 4dd115728..b0b4a4d66 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -403,8 +403,6 @@ const models: TsoaRoute.Models = { "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, - "organization": {"ref":"OrganizationRefID"}, - "organizationUnit": {"ref":"OrganizationUnitRefID"}, }, "additionalProperties": false, }, diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index 36d2f24ea..040e51f43 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -296,8 +296,6 @@ export interface Product { openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; - organization?: OrganizationRefID; - organizationUnit?: OrganizationUnitRefID; } From eab886cb72d4a39d709153f7ae7eb3e53de3c213 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 13:37:19 -0700 Subject: [PATCH 023/109] adj env spec --- src/batch/data-rules.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index f1d994793..194629e0e 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -464,12 +464,6 @@ const metadata = { refKey: 'name', filterByNamespace: true, }, - spec: { - name: 'connectExclusiveOne', - list: 'Blob', - refKey: 'ref', - filterByNamespace: true, - }, }, validations: { active: { type: 'boolean' }, From ca6075cbdb22867f167d78bb658c1684718d6fc1 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 14:01:16 -0700 Subject: [PATCH 024/109] adj env spec --- src/batch/data-rules.js | 9 +++++++-- src/controllers/v2/openapi.yaml | 8 ++++++-- src/controllers/v2/routes.ts | 8 +++++++- src/controllers/v2/types.ts | 8 +++++++- src/controllers/v3/openapi.yaml | 8 ++++++-- src/controllers/v3/routes.ts | 8 +++++++- src/controllers/v3/types.ts | 8 +++++++- 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 194629e0e..344b6f796 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -408,7 +408,7 @@ const metadata = { query: 'allProducts', refKey: 'appId', compositeRefKey: ['name', 'namespace'], - sync: ['name', 'type', 'description', 'namespace', 'openapiSpecs'], + sync: ['name', 'type', 'description', 'namespace', 'organization', 'openapiSpecs'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, openapiSpecs: { name: "toStringDefaultArray" }, @@ -417,7 +417,12 @@ const metadata = { list: 'Environment', syncFirst: true, refKey: 'appId', - } + }, + organization: { + name: 'connectOne', + list: 'allOrganizations', + refKey: 'name', + }, }, validations: { type: { diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 2251a9256..ba1a8eff2 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -594,6 +594,8 @@ components: type: string LegalRefID: type: string + BlobRefID: + type: string CredentialIssuerRefID: type: string Environment: @@ -624,14 +626,14 @@ components: - kong-api-key-acl additionalDetailsToRequest: type: string - spec: - type: string services: items: $ref: '#/components/schemas/GatewayServiceRefID' type: array legal: $ref: '#/components/schemas/LegalRefID' + spec: + $ref: '#/components/schemas/BlobRefID' credentialIssuer: $ref: '#/components/schemas/CredentialIssuerRefID' type: object @@ -667,6 +669,8 @@ components: items: $ref: '#/components/schemas/Environment' type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' type: object additionalProperties: false example: diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 8c06c1a51..674b8af49 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -388,6 +388,11 @@ const models: TsoaRoute.Models = { "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 + "BlobRefID": { + "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 "CredentialIssuerRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -402,9 +407,9 @@ const models: TsoaRoute.Models = { "approval": {"dataType":"boolean"}, "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, "additionalDetailsToRequest": {"dataType":"string"}, - "spec": {"dataType":"string"}, "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, "legal": {"ref":"LegalRefID"}, + "spec": {"ref":"BlobRefID"}, "credentialIssuer": {"ref":"CredentialIssuerRefID"}, }, "additionalProperties": false, @@ -421,6 +426,7 @@ const models: TsoaRoute.Models = { "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + "organization": {"ref":"OrganizationRefID"}, }, "additionalProperties": false, }, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index 72677bc4a..1ed2543bb 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -296,6 +296,7 @@ export interface Product { openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; + organization?: OrganizationRefID; } @@ -316,9 +317,9 @@ export interface Environment { approval?: boolean; flow?: "public" | "protected-externally" | "authorization-code" | "client-credentials" | "kong-acl-only" | "kong-api-key-only" | "kong-api-key-acl"; additionalDetailsToRequest?: string; - spec?: string; services?: GatewayServiceRefID[]; legal?: LegalRefID; + spec?: BlobRefID; credentialIssuer?: CredentialIssuerRefID; } @@ -546,6 +547,11 @@ export interface DatasetResource { */ export type ApplicationRefID = string +/** + * @tsoaModel + */ +export type BlobRefID = string + /** * @tsoaModel */ diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 07f64e999..01d0026f2 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -536,6 +536,8 @@ components: type: string LegalRefID: type: string + BlobRefID: + type: string CredentialIssuerRefID: type: string Environment: @@ -566,14 +568,14 @@ components: - kong-api-key-acl additionalDetailsToRequest: type: string - spec: - type: string services: items: $ref: '#/components/schemas/GatewayServiceRefID' type: array legal: $ref: '#/components/schemas/LegalRefID' + spec: + $ref: '#/components/schemas/BlobRefID' credentialIssuer: $ref: '#/components/schemas/CredentialIssuerRefID' type: object @@ -609,6 +611,8 @@ components: items: $ref: '#/components/schemas/Environment' type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' type: object additionalProperties: false example: diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index b0b4a4d66..083b1932f 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -370,6 +370,11 @@ const models: TsoaRoute.Models = { "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 + "BlobRefID": { + "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 "CredentialIssuerRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -384,9 +389,9 @@ const models: TsoaRoute.Models = { "approval": {"dataType":"boolean"}, "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, "additionalDetailsToRequest": {"dataType":"string"}, - "spec": {"dataType":"string"}, "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, "legal": {"ref":"LegalRefID"}, + "spec": {"ref":"BlobRefID"}, "credentialIssuer": {"ref":"CredentialIssuerRefID"}, }, "additionalProperties": false, @@ -403,6 +408,7 @@ const models: TsoaRoute.Models = { "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, "dataset": {"ref":"DraftDatasetRefID"}, "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + "organization": {"ref":"OrganizationRefID"}, }, "additionalProperties": false, }, diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index 040e51f43..91f9f2ade 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -296,6 +296,7 @@ export interface Product { openapiSpecs?: string[]; dataset?: DraftDatasetRefID; environments?: Environment[]; + organization?: OrganizationRefID; } @@ -316,9 +317,9 @@ export interface Environment { approval?: boolean; flow?: "public" | "protected-externally" | "authorization-code" | "client-credentials" | "kong-acl-only" | "kong-api-key-only" | "kong-api-key-acl"; additionalDetailsToRequest?: string; - spec?: string; services?: GatewayServiceRefID[]; legal?: LegalRefID; + spec?: BlobRefID; credentialIssuer?: CredentialIssuerRefID; } @@ -546,6 +547,11 @@ export interface DatasetResource { */ export type ApplicationRefID = string +/** + * @tsoaModel + */ +export type BlobRefID = string + /** * @tsoaModel */ From 068ae88f0e54c4de64860ea77df2e46fcb54ba98 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 14:03:25 -0700 Subject: [PATCH 025/109] include environments in products list --- src/controllers/v3/OrgProductController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 7d5009a11..235765272 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -63,7 +63,7 @@ export class OrgProductController extends Controller { ctx, 'Product', undefined, - [], + ['environments'], batchClause ); From fffb9eb284c145052143fa8054930e11d6cbe64e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 21 Aug 2025 14:23:00 -0700 Subject: [PATCH 026/109] set org for product --- src/controllers/v3/OrgProductController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 235765272..2221e4861 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -98,6 +98,7 @@ export class OrgProductController extends Controller { ): Promise { // TODO: Make sure namespace is allowed for this org body['gatewayId'] = gatewayId; + body['organization'] = org; return await syncRecordsThrowErrors( this.keystone.createContext(request, true), From 23743f4f0d8cdfe90ff6d900d4a432e23b03949e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 14:28:59 -0700 Subject: [PATCH 027/109] add client certificate as an option --- .../authorization-profile-controls/authentication.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nextapp/components/authorization-profile-controls/authentication.tsx b/src/nextapp/components/authorization-profile-controls/authentication.tsx index 760d9423d..1544085e2 100644 --- a/src/nextapp/components/authorization-profile-controls/authentication.tsx +++ b/src/nextapp/components/authorization-profile-controls/authentication.tsx @@ -108,6 +108,9 @@ const AuthorizationProfileAuthentication: React.FC Signed JWT with JWKS URL + + Client Certificate (mTLS) + From f2fd812c61310517b642ee98628f3b3eaace79b3 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 14:40:24 -0700 Subject: [PATCH 028/109] support client certificate --- .../keycloak/client-registration-service.ts | 9 ++ .../client-template-client-certificate.ts | 65 +++++++++++++ src/services/keystone/access-request.ts | 95 ++++++++++++++++++- src/services/keystone/application.ts | 23 +++++ .../integrated/keystonejs/accessRequest.ts | 72 ++++++++++++-- 5 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 src/services/keycloak/templates/client-template-client-certificate.ts diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index 7169d2e49..a58f32919 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -7,6 +7,7 @@ import { headers } from './keycloak-api'; import { strict as assert } from 'assert'; import { clientTemplateClientSecret } from './templates/client-template-client-secret'; +import { clientTemplateClientCertificate } from './templates/client-template-client-certificate'; import { clientTemplateClientJwt } from './templates/client-template-client-jwt'; import { clientTemplateSharedIdP } from './templates/client-template-shared-idp'; import { clientTemplateSharedIdPAuthz } from './templates/client-template-shared-idp-authz'; @@ -38,6 +39,7 @@ export enum ClientAuthenticator { ClientJWT = 'client-jwt', ClientJWTwithJWKS = 'client-jwt-jwks-url', ClientSecret = 'client-secret', + ClientCertificate = 'client-certificate', SharedIdP = 'shared-idp', SharedIdPWithAuthz = 'shared-idp-authz', } @@ -94,6 +96,13 @@ export class KeycloakClientRegistrationService { }, }); break; + case ClientAuthenticator.ClientCertificate: + body = Object.assign(JSON.parse(clientTemplateClientCertificate), { + enabled, + clientId, + secret: clientSecret, + }); + break; case ClientAuthenticator.SharedIdP: body = Object.assign(JSON.parse(clientTemplateSharedIdP), { enabled, diff --git a/src/services/keycloak/templates/client-template-client-certificate.ts b/src/services/keycloak/templates/client-template-client-certificate.ts new file mode 100644 index 000000000..237ce1762 --- /dev/null +++ b/src/services/keycloak/templates/client-template-client-certificate.ts @@ -0,0 +1,65 @@ + +export const clientTemplateClientCertificate = JSON.stringify({ + clientId: '', + name: '', + description: '', + surrogateAuthRequired: false, + enabled: false, + alwaysDisplayInConsole: false, + clientAuthenticatorType: 'client-x509', + redirectUris: ['http://*', 'https://*'], + webOrigins: ['*'], + notBefore: 0, + bearerOnly: false, + consentRequired: false, + standardFlowEnabled: true, + implicitFlowEnabled: false, + directAccessGrantsEnabled: false, + serviceAccountsEnabled: true, + publicClient: false, + frontchannelLogout: false, + protocol: 'openid-connect', + attributes: { + "request.object.signature.alg": "any", + "saml.multivalued.roles": "false", + "saml.force.post.binding": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml.client.signature": "false", + "require.pushed.authorization.requests": "false", + "request.object.encryption.enc": "any", + "dpop.bound.access.tokens": "false", + "saml.assertion.signature": "false", + "x509.subjectdn": "CN=sdx-pub-icbc-sap.api.gov.bc.ca", + "request.object.encryption.alg": "any", + "client.introspection.response.allow.jwt.claim.enabled": "false", + "saml.encrypt": "false", + "standard.token.exchange.enabled": "true", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "client.use.lightweight.access.token.enabled": "false", + "request.object.required": "not required", + "saml_force_name_id_format": "false", + "access.token.header.type.rfc9068": "false", + "acr.loa.map": "{}", + "tls.client.certificate.bound.access.tokens": "true", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "x509.allow.regex.pattern.comparison": "false", + "token.response.type.bearer.lower-case": "false", + "saml.onetimeuse.condition": "false" + }, + authenticationFlowBindingOverrides: {}, + fullScopeAllowed: false, + nodeReRegistrationTimeout: -1, + protocolMappers: [] as any[], + defaultClientScopes: [] as string[], + optionalClientScopes: [] as string[], + access: { view: true, configure: true, manage: true }, +}); diff --git a/src/services/keystone/access-request.ts b/src/services/keystone/access-request.ts index 289bbd2c0..fa57a2489 100644 --- a/src/services/keystone/access-request.ts +++ b/src/services/keystone/access-request.ts @@ -1,10 +1,103 @@ import { gql } from 'graphql-request'; import { Logger } from '../../logger'; -import { AccessRequest, AccessRequestUpdateInput } from './types'; +import { + AccessRequest, + AccessRequestCreateInput, + AccessRequestUpdateInput, +} from './types'; const assert = require('assert').strict; const logger = Logger('keystone.access-req'); +/* +acceptLegal +: +false +additionalDetails +: +"" +applicationId +: +"2" +controls +: +"{\"clientGenCertificate\":false,\"jwksUrl\":\"\",\"clientCertificate\":\"\"}" +name +: +"Sample API FOR Cope, Aidan CITZ:EX" +productEnvironmentId +: +"12" +requestor +: +"12"*/ + +export async function addAccessRequest( + context: any, + data: any +): Promise { + const query = gql` + mutation AddAccessRequest( + $name: String! + $controls: String + $requestor: ID! + $applicationId: ID! + $productEnvironmentId: ID! + $additionalDetails: String + $acceptLegal: Boolean! + ) { + acceptLegal( + productEnvironmentId: $productEnvironmentId + acceptLegal: $acceptLegal + ) { + legalsAgreed + } + + createAccessRequest( + data: { + name: $name + controls: $controls + additionalDetails: $additionalDetails + requestor: { connect: { id: $requestor } } + application: { connect: { id: $applicationId } } + productEnvironment: { connect: { id: $productEnvironmentId } } + } + ) { + id + } + } + `; + + logger.debug('Mutation [addAccessRequest] data %j', data); + const result = await context.executeGraphQL({ + query, + variables: { ...data }, + }); + logger.debug('Mutation [addAccessRequest] result %j', result); + return result.data.createAccessRequest; +} + +export async function collectCredentials(context: any, id: string): Promise { + logger.debug('Collecting credentials for access request %s', id); + const query = gql` + mutation genCredential($id: ID!) { + updateAccessRequest(id: $id, data: { credential: "NEW" }) { + credential + } + }` + const result = await context.executeGraphQL({ + query, + variables: { id }, + }); + logger.debug('Mutation [collectCredentials] result %j', result); + assert.strictEqual( + 'errors' in result, + false, + 'Error collecting credentials' + ); + return result.data.updateAccessRequest; +} + export async function getAccessRequestsByNamespace( context: any, ns: string diff --git a/src/services/keystone/application.ts b/src/services/keystone/application.ts index ac9f7142c..8e15a6849 100644 --- a/src/services/keystone/application.ts +++ b/src/services/keystone/application.ts @@ -41,3 +41,26 @@ export async function lookupMyApplicationsById( logger.debug('[lookupMyApplicationsById] result %j', result); return result.data.myApplications[0]; } + + +export async function createApplication( + context: any, + data: { name: string, ownerId: string, description?: string } +): Promise { + logger.debug('[createApplication] %j', data); + const result = await context.executeGraphQL({ + query: `mutation CreateApplication($name: String!, $description: String, $ownerId: ID!) { + createApplication(data: {name: $name, owner: {connect: {id: $ownerId}}, description: $description}) { + id + appId + name + owner { + name + } + } + }`, + variables: data, + }); + logger.debug('[createApplication] result %j', result); + return result.data.createApplication; +} \ No newline at end of file diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 0ecade6ae..84e7661bf 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -4,17 +4,45 @@ To run: npm run ts-build npm run ts-watch node dist/test/integrated/keystonejs/accessRequest.js + + +NEEDS: + +kubectl port-forward -n 1d4461-prod service/patroni-spilo 15432:5432 & + +export ADAPTER=knex +export KNEX_DATABASE=keystonejs +export KNEX_HOST=localhost +export KNEX_PORT=15432 +export KNEX_USER=keystonejsuser +export KNEX_PASSWORD= + + +export KONG_URL="https://kong-admin-api-1d4461-prod.apps.silver.devops.gov.bc.ca" + +kubectl port-forward -n 1d4461-prod service/bcgov-aps-portal-feeder-generic-api 6767:80 & + +export FEEDER_URL=http://localhost:6767 + +// userId is needed for Legal +// namespace has to match requesting product if not published + */ import InitKeystone from './init'; import { o } from '../util'; -import { getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request'; +import { addAccessRequest, collectCredentials, getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request'; +import { add } from 'lodash'; +import { AccessRequestCreateInput } from 'apis/shared/types/query.types'; +import { createApplication } from '../../../services/keystone/application'; (async () => { const keystone = await InitKeystone(); - const ns = 'gw-0dcd7'; - const skipAccessControl = false; + const ns = 'gw-0a524'; + const skipAccessControl = true; + + const userId = '12'; const identity = { id: null, @@ -22,7 +50,7 @@ import { getOpenAccessRequestsByConsumer } from '../../../services/keystone/acce namespace: ns, roles: JSON.stringify(['api-owner']), scopes: [], - userId: null, + userId, } as any; const ctx = keystone.createContext({ @@ -32,12 +60,36 @@ import { getOpenAccessRequestsByConsumer } from '../../../services/keystone/acce // o(await getOrganizations(ctx)); - const serviceAccess = await getOpenAccessRequestsByConsumer( - ctx, - ns, - '653860ee26683257394cfe3c' - ); - o(serviceAccess); + const accessRequestData = { + acceptLegal: false, + additionalDetails: '', + //applicationId: '5', // App2 + controls: '{"clientGenCertificate":false,"jwksUrl":"","clientCertificate":""}', + name: 'Sample API FOR Cope, Aidan CITZ:EX', + productEnvironmentId: '13', + requestor: userId, + } as any; + + + // userId is needed for Legal + + const app = await createApplication(ctx, { name: 'App', description: 'App Desc', ownerId: userId }); + + accessRequestData.applicationId = app.id; + + const result = await addAccessRequest(ctx, accessRequestData); + o(result); + + const creds = await collectCredentials(ctx, result.id); + o(creds); + + // const serviceAccess = await getOpenAccessRequestsByConsumer( + // ctx, + // ns, + // '653860ee26683257394cfe3c' + // ); + // o(serviceAccess); + await keystone.disconnect(); })(); From c39374c48bff01319e976b7b52f127248e31af55 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 14:49:02 -0700 Subject: [PATCH 029/109] add client certificate support --- src/services/keycloak/client-registration-service.ts | 11 ++++++++++- .../templates/client-template-client-certificate.ts | 2 +- src/services/workflow/client-credentials.ts | 4 ++++ src/services/workflow/client-shared-idp.ts | 2 ++ src/services/workflow/types.ts | 1 + .../services/keycloak/client-registration.test.ts | 4 ++++ 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index a58f32919..67b42ff4b 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -71,8 +71,10 @@ export class KeycloakClientRegistrationService { public async clientRegistration( authenticator: ClientAuthenticator, clientId: string, + name: string, clientSecret: string, certificate: string, + subjectDn: string, jwksUrl: string, clientMappers: ClientMapper[], enabled: boolean = false, @@ -83,6 +85,7 @@ export class KeycloakClientRegistrationService { case ClientAuthenticator.ClientSecret: body = Object.assign(JSON.parse(clientTemplateClientSecret), { enabled, + name, clientId, secret: clientSecret, }); @@ -90,6 +93,7 @@ export class KeycloakClientRegistrationService { case ClientAuthenticator.ClientJWT: body = Object.assign(JSON.parse(clientTemplateClientJwt), { enabled, + name, clientId, attributes: { 'jwt.credential.public.key': certificate, @@ -99,13 +103,17 @@ export class KeycloakClientRegistrationService { case ClientAuthenticator.ClientCertificate: body = Object.assign(JSON.parse(clientTemplateClientCertificate), { enabled, + name, clientId, - secret: clientSecret, + attributes: { + 'x509.subjectdn': subjectDn + } }); break; case ClientAuthenticator.SharedIdP: body = Object.assign(JSON.parse(clientTemplateSharedIdP), { enabled, + name, clientId, baseUrl, attributes: {}, @@ -114,6 +122,7 @@ export class KeycloakClientRegistrationService { case ClientAuthenticator.SharedIdPWithAuthz: body = Object.assign(JSON.parse(clientTemplateSharedIdPAuthz), { enabled, + name, clientId, baseUrl, attributes: {}, diff --git a/src/services/keycloak/templates/client-template-client-certificate.ts b/src/services/keycloak/templates/client-template-client-certificate.ts index 237ce1762..6198f39af 100644 --- a/src/services/keycloak/templates/client-template-client-certificate.ts +++ b/src/services/keycloak/templates/client-template-client-certificate.ts @@ -36,7 +36,7 @@ export const clientTemplateClientCertificate = JSON.stringify({ "request.object.encryption.enc": "any", "dpop.bound.access.tokens": "false", "saml.assertion.signature": "false", - "x509.subjectdn": "CN=sdx-pub-icbc-sap.api.gov.bc.ca", + "x509.subjectdn": "", "request.object.encryption.alg": "any", "client.introspection.response.allow.jwt.claim.enabled": "false", "saml.encrypt": "false", diff --git a/src/services/workflow/client-credentials.ts b/src/services/workflow/client-credentials.ts index 8830a3192..bfc4363db 100644 --- a/src/services/workflow/client-credentials.ts +++ b/src/services/workflow/client-credentials.ts @@ -77,6 +77,8 @@ export async function registerClient( // Find the Client ID for the ProductEnvironment - that will be used to associated the clientRoles + issuer.clientAuthenticator = ClientAuthenticator.ClientCertificate; + // lookup Application and use the ID to make sure a corresponding Consumer exists (1 -- 1) const client = await new KeycloakClientRegistrationService( issuerEnvConfig.issuerUrl, @@ -85,8 +87,10 @@ export async function registerClient( ).clientRegistration( issuer.clientAuthenticator, newClientId, + '', uuidv4(), controls.clientCertificate, + controls.subjectDn, controls.jwksUrl, clientMappers, false diff --git a/src/services/workflow/client-shared-idp.ts b/src/services/workflow/client-shared-idp.ts index 984ee1b1e..c06a1cca5 100644 --- a/src/services/workflow/client-shared-idp.ts +++ b/src/services/workflow/client-shared-idp.ts @@ -119,8 +119,10 @@ async function addClientToSharedIdP( ).clientRegistration( clientAuthenticator, clientId, + '', uuidv4(), controls.clientCertificate, + controls.subjectDn, controls.jwksUrl, clientMappers, true, diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index a00f452a9..6910060bd 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -51,6 +51,7 @@ export interface RequestControls { plugins?: ConsumerPlugin[]; clientCertificate?: string; clientGenCertificate?: boolean; + subjectDn?: string; // Subject DN for the client certificate jwksUrl?: string; subject?: SubjectIdentity; } diff --git a/src/test/services/keycloak/client-registration.test.ts b/src/test/services/keycloak/client-registration.test.ts index 9922af236..b5485c555 100644 --- a/src/test/services/keycloak/client-registration.test.ts +++ b/src/test/services/keycloak/client-registration.test.ts @@ -14,8 +14,10 @@ describe('Keycloak Service', function () { const result = await regsvc.clientRegistration( ClientAuthenticator.ClientSecret, 'cid', + 'nm', 'csc', 'cert', + 'subdn', 'jwks', [], true @@ -32,8 +34,10 @@ describe('Keycloak Service', function () { const result = await regsvc.clientRegistration( ClientAuthenticator.ClientJWTwithJWKS, 'cid', + 'nm', 'csc', 'cert', + 'subdn', 'jwks', [], true From dea1408177c3526d2ef140e300413860ba731e4e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 14:55:58 -0700 Subject: [PATCH 030/109] add subjectdn to results --- src/services/workflow/generate-credential.ts | 1 + src/services/workflow/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/services/workflow/generate-credential.ts b/src/services/workflow/generate-credential.ts index 3fb2466bd..18cca8b13 100644 --- a/src/services/workflow/generate-credential.ts +++ b/src/services/workflow/generate-credential.ts @@ -204,6 +204,7 @@ export const generateCredential = async ( tokenEndpoint: newClient.openid.token_endpoint, clientPublicKey: clientSigning.publicKey, clientPrivateKey: clientSigning.privateKey, + subjectDn: controls.subjectDn ? controls.subjectDn : null, } as NewCredential; } return null; diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index 6910060bd..12a62b34b 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -20,6 +20,7 @@ export interface NewCredential { apiKey?: string; clientPublicKey?: string; clientPrivateKey?: string; + subjectDn?: string; // Subject DN for the client certificate } export interface CredentialReference { From 857a34201b158814fa95603b5946d4faf6b14830 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 15:26:32 -0700 Subject: [PATCH 031/109] upd activity details --- src/services/keystone/access-request.ts | 47 ++++++++++--------- src/services/keystone/activity.ts | 2 +- src/services/workflow/types.ts | 2 +- .../integrated/keystonejs/accessRequest.ts | 44 +++++++++++++++-- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/services/keystone/access-request.ts b/src/services/keystone/access-request.ts index fa57a2489..a7864614f 100644 --- a/src/services/keystone/access-request.ts +++ b/src/services/keystone/access-request.ts @@ -9,29 +9,6 @@ import { const assert = require('assert').strict; const logger = Logger('keystone.access-req'); -/* -acceptLegal -: -false -additionalDetails -: -"" -applicationId -: -"2" -controls -: -"{\"clientGenCertificate\":false,\"jwksUrl\":\"\",\"clientCertificate\":\"\"}" -name -: -"Sample API FOR Cope, Aidan CITZ:EX" -productEnvironmentId -: -"12" -requestor -: -"12"*/ - export async function addAccessRequest( context: any, data: any @@ -98,6 +75,30 @@ export async function collectCredentials(context: any, id: string): Promise { + const query = gql` + query GetAccessRequestById($id: ID!) { + AccessRequest(where: { id: $id }) { + id + name + isApproved + isIssued + isComplete + serviceAccess { + id + } + } + } + `; + + const result = await context.executeGraphQL({ + query, + variables: { id }, + }); + logger.debug('Query [getAccessRequest] result %j', result); + return result.data.AccessRequest; +} + export async function getAccessRequestsByNamespace( context: any, ns: string diff --git a/src/services/keystone/activity.ts b/src/services/keystone/activity.ts index 89b01cf54..cd50db967 100644 --- a/src/services/keystone/activity.ts +++ b/src/services/keystone/activity.ts @@ -133,7 +133,7 @@ export async function recordActivity( ? productNamespace : context.authedItem.namespace; const name = `${action} ${type}[${refId}]`; - logger.debug('[recordActivity] userid=%s name=%s', userId, name); + logger.debug('[recordActivity] ns=%s userid=%s name=%s', namespace, userId, name); const variables: { [key: string]: any } = { name, diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index 12a62b34b..78b93ee4b 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -20,7 +20,7 @@ export interface NewCredential { apiKey?: string; clientPublicKey?: string; clientPrivateKey?: string; - subjectDn?: string; // Subject DN for the client certificate + subjectDn?: string; // Subject DN for the client certificate, } export interface CredentialReference { diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 84e7661bf..244d8237d 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -31,10 +31,12 @@ export FEEDER_URL=http://localhost:6767 import InitKeystone from './init'; import { o } from '../util'; -import { addAccessRequest, collectCredentials, getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request'; +import { addAccessRequest, collectCredentials, getAccessRequest, getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request'; import { add } from 'lodash'; import { AccessRequestCreateInput } from 'apis/shared/types/query.types'; import { createApplication } from '../../../services/keystone/application'; +import { deleteServiceAccess } from '../../../services/keystone'; +import { revokeAllConsumerAccess } from '../../../services/workflow'; (async () => { const keystone = await InitKeystone(); @@ -47,6 +49,7 @@ import { createApplication } from '../../../services/keystone/application'; const identity = { id: null, username: 'sample_username', + name: "SampleF UserL", namespace: ns, roles: JSON.stringify(['api-owner']), scopes: [], @@ -62,9 +65,10 @@ import { createApplication } from '../../../services/keystone/application'; const accessRequestData = { acceptLegal: false, - additionalDetails: '', + additionalDetails: 'here is some additional details', //applicationId: '5', // App2 - controls: '{"clientGenCertificate":false,"jwksUrl":"","clientCertificate":""}', + //controls: '{"clientGenCertificate":false,"jwksUrl":"","clientCertificate":""}', + controls: JSON.stringify({ "jwksUrl":"",subjectDn: "CN=my-site"}), name: 'Sample API FOR Cope, Aidan CITZ:EX', productEnvironmentId: '13', requestor: userId, @@ -81,8 +85,40 @@ import { createApplication } from '../../../services/keystone/application'; o(result); const creds = await collectCredentials(ctx, result.id); - o(creds); + o(JSON.parse(creds.credential)); +// query +// : +// "\n mutation SaveConsumerLabels($consumerId: ID!, $labels: [JSON]) {\n saveConsumerLabels(consumerId: $consumerId, labels: $labels)\n }\n" +// variables +// : +// {consumerId: "27",…} +// consumerId +// : +// "27" +// labels +// : +// [{labelGroup: "Priority", values: ["Mister"]}, {labelGroup: "", values: []}] + + + const request = await getAccessRequest(ctx, result.id); + o(request); + + const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); + o(revoke); + + // const revoke = await deleteServiceAccess(ctx, request.serviceAccess.id); + // o(revoke); + +// flow: client-credentials +// clientId: 50C1D755-945C1E80ABB +// clientSecret: null +// issuer: null +// tokenEndpoint: >- +// https://sdx-authz-apps-gov-bc-ca-lab.apps.gov.bc.ca/auth/realms/sdx/protocol/openid-connect/token +// clientPublicKey: null +// clientPrivateKey: null + // const serviceAccess = await getOpenAccessRequestsByConsumer( // ctx, // ns, From 1c3429b05999eb8e678c51f5dc04e2fe50f2ee68 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 15:27:28 -0700 Subject: [PATCH 032/109] remove hardcoded item --- src/services/workflow/client-credentials.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/workflow/client-credentials.ts b/src/services/workflow/client-credentials.ts index bfc4363db..404694010 100644 --- a/src/services/workflow/client-credentials.ts +++ b/src/services/workflow/client-credentials.ts @@ -77,8 +77,6 @@ export async function registerClient( // Find the Client ID for the ProductEnvironment - that will be used to associated the clientRoles - issuer.clientAuthenticator = ClientAuthenticator.ClientCertificate; - // lookup Application and use the ID to make sure a corresponding Consumer exists (1 -- 1) const client = await new KeycloakClientRegistrationService( issuerEnvConfig.issuerUrl, From 9ad0cb41c1896f292417ca2c25b305738b0ad1f5 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 15:33:08 -0700 Subject: [PATCH 033/109] upd test --- src/test/integrated/keystonejs/accessRequest.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 244d8237d..649cc606e 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -31,11 +31,8 @@ export FEEDER_URL=http://localhost:6767 import InitKeystone from './init'; import { o } from '../util'; -import { addAccessRequest, collectCredentials, getAccessRequest, getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request'; -import { add } from 'lodash'; -import { AccessRequestCreateInput } from 'apis/shared/types/query.types'; +import { addAccessRequest, collectCredentials, getAccessRequest } from '../../../services/keystone/access-request'; import { createApplication } from '../../../services/keystone/application'; -import { deleteServiceAccess } from '../../../services/keystone'; import { revokeAllConsumerAccess } from '../../../services/workflow'; (async () => { From b4bfbc39abed913d0d6444c25f6942fe5dbcb0f2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 15:57:20 -0700 Subject: [PATCH 034/109] add new dropdown to crediss --- .../authorization-profile-form/authentication-form.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nextapp/components/authorization-profile-form/authentication-form.tsx b/src/nextapp/components/authorization-profile-form/authentication-form.tsx index 96f893c30..6bc5902a4 100644 --- a/src/nextapp/components/authorization-profile-form/authentication-form.tsx +++ b/src/nextapp/components/authorization-profile-form/authentication-form.tsx @@ -82,6 +82,14 @@ const AuthenticationForm: React.FC = ({ Client Credential Flow, using signed JWT with JWKS URL or Public Key + + Client Credential Flow, using x509 Certificate + + From f105c137a86ae74ae2d08b2ad3b6d25e8026c596 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 15:57:48 -0700 Subject: [PATCH 035/109] more integration testing --- src/services/keystone/access-request.ts | 24 ++++++ .../integrated/keystonejs/accessRequest.ts | 84 +++++++++++-------- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/services/keystone/access-request.ts b/src/services/keystone/access-request.ts index a7864614f..729440e23 100644 --- a/src/services/keystone/access-request.ts +++ b/src/services/keystone/access-request.ts @@ -84,8 +84,32 @@ export async function getAccessRequest(context: any, id: string): Promise { const keystone = await InitKeystone(); @@ -46,7 +53,7 @@ import { revokeAllConsumerAccess } from '../../../services/workflow'; const identity = { id: null, username: 'sample_username', - name: "SampleF UserL", + name: 'SampleF UserL', namespace: ns, roles: JSON.stringify(['api-owner']), scopes: [], @@ -65,16 +72,19 @@ import { revokeAllConsumerAccess } from '../../../services/workflow'; additionalDetails: 'here is some additional details', //applicationId: '5', // App2 //controls: '{"clientGenCertificate":false,"jwksUrl":"","clientCertificate":""}', - controls: JSON.stringify({ "jwksUrl":"",subjectDn: "CN=my-site"}), + controls: JSON.stringify({ jwksUrl: '', subjectDn: 'CN=my-site' }), name: 'Sample API FOR Cope, Aidan CITZ:EX', productEnvironmentId: '13', requestor: userId, } as any; - // userId is needed for Legal - const app = await createApplication(ctx, { name: 'App', description: 'App Desc', ownerId: userId }); + const app = await createApplication(ctx, { + name: 'App ' + new Date().toISOString(), + description: 'App Desc', + ownerId: userId, + }); accessRequestData.applicationId = app.id; @@ -82,39 +92,46 @@ import { revokeAllConsumerAccess } from '../../../services/workflow'; o(result); const creds = await collectCredentials(ctx, result.id); - o(JSON.parse(creds.credential)); - -// query -// : -// "\n mutation SaveConsumerLabels($consumerId: ID!, $labels: [JSON]) {\n saveConsumerLabels(consumerId: $consumerId, labels: $labels)\n }\n" -// variables -// : -// {consumerId: "27",…} -// consumerId -// : -// "27" -// labels -// : -// [{labelGroup: "Priority", values: ["Mister"]}, {labelGroup: "", values: []}] - + const credDetails = JSON.parse(creds.credential); + o(credDetails); + + // query + // : + // "\n mutation SaveConsumerLabels($consumerId: ID!, $labels: [JSON]) {\n saveConsumerLabels(consumerId: $consumerId, labels: $labels)\n }\n" + // variables + // : + // {consumerId: "27",…} + // consumerId + // : + // "27" + // labels + // : + // [{labelGroup: "Priority", values: ["Mister"]}, {labelGroup: "", values: []}] + + const labels = [ + { labelGroup: 'sdx-member', values: ['/MIN/CITZ'] }, + { labelGroup: 'sdx-res-locator', values: ['/LAB/MIN/CITZ/MYSVC-API'] }, + ]; const request = await getAccessRequest(ctx, result.id); o(request); - const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); - o(revoke); - + await saveConsumerLabels(ctx, ns, request.serviceAccess.consumer.id, labels); + + // const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); + // o(revoke); + // const revoke = await deleteServiceAccess(ctx, request.serviceAccess.id); // o(revoke); - -// flow: client-credentials -// clientId: 50C1D755-945C1E80ABB -// clientSecret: null -// issuer: null -// tokenEndpoint: >- -// https://sdx-authz-apps-gov-bc-ca-lab.apps.gov.bc.ca/auth/realms/sdx/protocol/openid-connect/token -// clientPublicKey: null -// clientPrivateKey: null + + // flow: client-credentials + // clientId: 50C1D755-945C1E80ABB + // clientSecret: null + // issuer: null + // tokenEndpoint: >- + // https://sdx-authz-apps-gov-bc-ca-lab.apps.gov.bc.ca/auth/realms/sdx/protocol/openid-connect/token + // clientPublicKey: null + // clientPrivateKey: null // const serviceAccess = await getOpenAccessRequestsByConsumer( // ctx, @@ -123,6 +140,5 @@ import { revokeAllConsumerAccess } from '../../../services/workflow'; // ); // o(serviceAccess); - await keystone.disconnect(); })(); From 2d9b5a398ab0a3b0e04f305e9e6aa2338f8735e6 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 22 Aug 2025 17:27:44 -0700 Subject: [PATCH 036/109] workaround for token exchange --- src/package-lock.json | 269 +++++++++++++----- src/package.json | 2 +- .../keycloak/client-registration-service.ts | 1 + .../client-template-client-certificate.ts | 108 +++---- src/services/keystone/access-request.ts | 1 + src/services/workflow/apply.ts | 11 +- src/services/workflow/client-credentials.ts | 25 +- src/services/workflow/types.ts | 3 +- .../integrated/keystonejs/accessRequest.ts | 52 ++-- 9 files changed, 300 insertions(+), 172 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e452672db..ac260394a 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -13,7 +13,7 @@ "@chakra-ui/react": "^1.6.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@keycloak/keycloak-admin-client": "^17.0.0-dev.26", + "@keycloak/keycloak-admin-client": "^17.0.1", "@keystone-next/admin-ui": "^7.0.0", "@keystonejs/access-control": "^7.1.1", "@keystonejs/adapter-mongoose": "^11.2.2", @@ -60,6 +60,7 @@ "keycloak-connect": "^17.0.1", "lodash": "^4.17.21", "multer": "^1.4.2", + "node-fetch": "^2.7.0", "nodemailer": "^6.6.0", "npmlog": "^6.0.1", "numeral": "^2.0.6", @@ -153,7 +154,7 @@ "typescript": "4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=22.0.0 <23.0.0" } }, "node_modules/@ampproject/remapping": { @@ -5471,6 +5472,15 @@ "node-fetch": "2.6.1" } }, + "node_modules/@graphql-tools/links/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@graphql-tools/load": { "version": "6.2.8", "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.8.tgz", @@ -5841,6 +5851,15 @@ "node-fetch": "2.6.1" } }, + "node_modules/@graphql-tools/url-loader/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@graphql-tools/url-loader/node_modules/tslib": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", @@ -7423,6 +7442,7 @@ "version": "17.0.1", "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-17.0.1.tgz", "integrity": "sha512-lgw6P7pGcJQbJExCE6+FpznDjadI571rCoSj2CmE8KAHu0BCsj2eqWP9HI3wb6zpkJwVQ86uLHTcsGvS8Ijjtw==", + "license": "Apache-2.0", "dependencies": { "axios": "^0.25.0", "camelize-ts": "^1.0.8", @@ -9512,6 +9532,15 @@ } } }, + "node_modules/@keystonejs/app-admin-ui/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@keystonejs/app-admin-ui/node_modules/pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -10239,6 +10268,15 @@ } } }, + "node_modules/@keystonejs/fields-auto-increment/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@keystonejs/fields-auto-increment/node_modules/pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -11060,6 +11098,15 @@ } } }, + "node_modules/@keystonejs/fields-mongoid/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@keystonejs/fields-mongoid/node_modules/pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -11804,6 +11851,15 @@ } } }, + "node_modules/@keystonejs/fields/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@keystonejs/fields/node_modules/pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -12844,6 +12900,15 @@ "node": ">=8" } }, + "node_modules/@prisma/sdk/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@prisma/sdk/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -20557,6 +20622,15 @@ "uuid": "8.3.0" } }, + "node_modules/checkpoint-client/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/checkpoint-client/node_modules/uuid": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", @@ -22052,6 +22126,15 @@ "node-fetch": "2.6.1" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "license": "MIT", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -22078,30 +22161,6 @@ "web-streams-polyfill": "^3.2.0" } }, - "node_modules/cross-undici-fetch/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/cross-undici-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/cross-undici-fetch/node_modules/undici": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.0.0.tgz", @@ -22110,20 +22169,6 @@ "node": ">=12.18" } }, - "node_modules/cross-undici-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/cross-undici-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/cryptiles": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.2.tgz", @@ -37716,11 +37761,45 @@ } }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/node-fingerprint": { @@ -53337,6 +53416,11 @@ "requires": { "node-fetch": "2.6.1" } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" } } }, @@ -53666,6 +53750,11 @@ "node-fetch": "2.6.1" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "tslib": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", @@ -56513,6 +56602,11 @@ "tildify": "2.0.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -57232,6 +57326,11 @@ "tildify": "2.0.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -57651,6 +57750,11 @@ "tildify": "2.0.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -58311,6 +58415,11 @@ "tildify": "2.0.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -59114,6 +59223,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -65185,6 +65299,11 @@ "uuid": "8.3.0" }, "dependencies": { + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "uuid": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", @@ -66395,6 +66514,13 @@ "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==", "requires": { "node-fetch": "2.6.1" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } } }, "cross-spawn": { @@ -66420,37 +66546,10 @@ "web-streams-polyfill": "^3.2.0" }, "dependencies": { - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "undici": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.0.0.tgz", "integrity": "sha512-VhUpiZ3No1DOPPQVQnsDZyfcbTTcHdcgWej1PdFnSvOeJmOVDgiOHkunJmBLfmjt4CqgPQddPVjSWW0dsTs5Yg==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } } } }, @@ -78441,9 +78540,33 @@ } }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-fingerprint": { "version": "0.0.2", diff --git a/src/package.json b/src/package.json index d238add76..00f80e53c 100644 --- a/src/package.json +++ b/src/package.json @@ -63,7 +63,7 @@ "@chakra-ui/react": "^1.6.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@keycloak/keycloak-admin-client": "^17.0.0-dev.26", + "@keycloak/keycloak-admin-client": "^17.0.1", "@keystone-next/admin-ui": "^7.0.0", "@keystonejs/access-control": "^7.1.1", "@keystonejs/adapter-mongoose": "^11.2.2", diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index 67b42ff4b..714dcdcb6 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -33,6 +33,7 @@ export interface ClientRegistration { clientId: string; clientSecret?: string; enabled?: boolean; + attributes?: { [key: string]: string }; } export enum ClientAuthenticator { diff --git a/src/services/keycloak/templates/client-template-client-certificate.ts b/src/services/keycloak/templates/client-template-client-certificate.ts index 6198f39af..8e4d28620 100644 --- a/src/services/keycloak/templates/client-template-client-certificate.ts +++ b/src/services/keycloak/templates/client-template-client-certificate.ts @@ -1,65 +1,65 @@ export const clientTemplateClientCertificate = JSON.stringify({ - clientId: '', - name: '', - description: '', - surrogateAuthRequired: false, - enabled: false, + access: { view: true, configure: true, manage: true }, alwaysDisplayInConsole: false, - clientAuthenticatorType: 'client-x509', - redirectUris: ['http://*', 'https://*'], - webOrigins: ['*'], - notBefore: 0, - bearerOnly: false, - consentRequired: false, - standardFlowEnabled: true, - implicitFlowEnabled: false, - directAccessGrantsEnabled: false, - serviceAccountsEnabled: true, - publicClient: false, - frontchannelLogout: false, - protocol: 'openid-connect', + authenticationFlowBindingOverrides: {}, attributes: { - "request.object.signature.alg": "any", - "saml.multivalued.roles": "false", - "saml.force.post.binding": "false", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "realm_client": "false", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "saml.client.signature": "false", - "require.pushed.authorization.requests": "false", - "request.object.encryption.enc": "any", - "dpop.bound.access.tokens": "false", - "saml.assertion.signature": "false", - "x509.subjectdn": "", + "acr.loa.map": "{}", + "access.token.header.type.rfc9068": false, + "backchannel.logout.revoke.offline.tokens": false, + "backchannel.logout.session.required": true, + "client.introspection.response.allow.jwt.claim.enabled": false, + "client.use.lightweight.access.token.enabled": false, + "client_credentials.use_refresh_token": false, + "display.on.consent.screen": false, + "dpop.bound.access.tokens": false, + "exclude.session.state.from.auth.response": false, + "oauth2.device.authorization.grant.enabled": false, + "oidc.ciba.grant.enabled": false, + "realm_client": false, "request.object.encryption.alg": "any", - "client.introspection.response.allow.jwt.claim.enabled": "false", - "saml.encrypt": "false", - "standard.token.exchange.enabled": "true", - "saml.server.signature": "false", - "exclude.session.state.from.auth.response": "false", - "client.use.lightweight.access.token.enabled": "false", + "request.object.encryption.enc": "any", "request.object.required": "not required", - "saml_force_name_id_format": "false", - "access.token.header.type.rfc9068": "false", - "acr.loa.map": "{}", - "tls.client.certificate.bound.access.tokens": "true", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "x509.allow.regex.pattern.comparison": "false", - "token.response.type.bearer.lower-case": "false", - "saml.onetimeuse.condition": "false" + "request.object.signature.alg": "any", + "require.pushed.authorization.requests": false, + "saml.client.signature": false, + "saml.encrypt": false, + "saml.assertion.signature": false, + "saml.authnstatement": false, + "saml.force.post.binding": false, + "saml.multivalued.roles": false, + "saml.onetimeuse.condition": false, + "saml.server.signature": false, + "saml.server.signature.keyinfo.ext": false, + "saml_force_name_id_format": false, + "standard.token.exchange.enabled": true, + "tls.client.certificate.bound.access.tokens": true, + "token.response.type.bearer.lower-case": false, + "use.refresh.tokens": true, + "x509.allow.regex.pattern.comparison": false, + "x509.subjectdn": "" }, - authenticationFlowBindingOverrides: {}, + bearerOnly: false, + clientAuthenticatorType: 'client-x509', + clientId: '', + consentRequired: false, + defaultClientScopes: [] as string[], + description: '', + directAccessGrantsEnabled: false, + enabled: false, + frontchannelLogout: false, fullScopeAllowed: false, + implicitFlowEnabled: false, + name: '', nodeReRegistrationTimeout: -1, - protocolMappers: [] as any[], - defaultClientScopes: [] as string[], + notBefore: 0, optionalClientScopes: [] as string[], - access: { view: true, configure: true, manage: true }, + protocol: 'openid-connect', + protocolMappers: [] as any[], + publicClient: false, + redirectUris: ['http://*', 'https://*'], + serviceAccountsEnabled: false, + standardFlowEnabled: true, + surrogateAuthRequired: false, + webOrigins: ['*'], }); diff --git a/src/services/keystone/access-request.ts b/src/services/keystone/access-request.ts index 729440e23..ce4089ec2 100644 --- a/src/services/keystone/access-request.ts +++ b/src/services/keystone/access-request.ts @@ -90,6 +90,7 @@ export async function getAccessRequest(context: any, id: string): Promiseissuer.clientAuthenticator, newClientId, - '', + controls.clientName || "", // if no client name provided, use the clientId uuidv4(), controls.clientCertificate, controls.subjectDn, @@ -95,6 +100,18 @@ export async function registerClient( ); assert.strictEqual(client.clientId, newClientId); + if (issuer.clientAuthenticator === "client-certificate") { + logger.warn("Workaround to set standard.token.exchange.enabled for client-certificate - not setting on creation"); + regService.updateClientRegistration(newClientId, { + clientId: newClientId, + attributes: { + "standard.token.exchange.enabled": 'true', + "tls.client.certificate.bound.access.tokens": 'true', + //"dpop.bound.access.tokens": 'true', + } + }) + } + return { openid, client, diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index 78b93ee4b..e894bf25f 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -45,8 +45,9 @@ export interface SubjectIdentity { email?: string; } export interface RequestControls { + clientName?: string; defaultClientScopes?: string[]; - defaultOptionalScopes?: string[]; + optionalClientScopes?: string[]; roles?: string[]; aclGroups?: string[]; plugins?: ConsumerPlugin[]; diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 30d507a8a..2612a2a45 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -24,6 +24,7 @@ kubectl port-forward -n 1d4461-prod service/bcgov-aps-portal-feeder-generic-api export FEEDER_URL=http://localhost:6767 + // userId is needed for Legal // namespace has to match requesting product if not published @@ -66,26 +67,28 @@ import { }); // o(await getOrganizations(ctx)); + const app = await createApplication(ctx, { + name: 'App ' + new Date().toISOString(), + description: 'App Desc', + ownerId: userId, + }); + + const controls = { + clientName: app.name, + subjectDn: 'CN=my-site', + //defaultClientScopes: [], + optionalClientScopes: ['user/Test1'], + }; const accessRequestData = { acceptLegal: false, additionalDetails: 'here is some additional details', - //applicationId: '5', // App2 - //controls: '{"clientGenCertificate":false,"jwksUrl":"","clientCertificate":""}', - controls: JSON.stringify({ jwksUrl: '', subjectDn: 'CN=my-site' }), + controls: JSON.stringify(controls), name: 'Sample API FOR Cope, Aidan CITZ:EX', productEnvironmentId: '13', requestor: userId, } as any; - // userId is needed for Legal - - const app = await createApplication(ctx, { - name: 'App ' + new Date().toISOString(), - description: 'App Desc', - ownerId: userId, - }); - accessRequestData.applicationId = app.id; const result = await addAccessRequest(ctx, accessRequestData); @@ -95,27 +98,15 @@ import { const credDetails = JSON.parse(creds.credential); o(credDetails); - // query - // : - // "\n mutation SaveConsumerLabels($consumerId: ID!, $labels: [JSON]) {\n saveConsumerLabels(consumerId: $consumerId, labels: $labels)\n }\n" - // variables - // : - // {consumerId: "27",…} - // consumerId - // : - // "27" - // labels - // : - // [{labelGroup: "Priority", values: ["Mister"]}, {labelGroup: "", values: []}] + const request = await getAccessRequest(ctx, result.id); + o(request); const labels = [ { labelGroup: 'sdx-member', values: ['/MIN/CITZ'] }, { labelGroup: 'sdx-res-locator', values: ['/LAB/MIN/CITZ/MYSVC-API'] }, + { labelGroup: "application", values: [app.name] } ]; - const request = await getAccessRequest(ctx, result.id); - o(request); - await saveConsumerLabels(ctx, ns, request.serviceAccess.consumer.id, labels); // const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); @@ -124,15 +115,6 @@ import { // const revoke = await deleteServiceAccess(ctx, request.serviceAccess.id); // o(revoke); - // flow: client-credentials - // clientId: 50C1D755-945C1E80ABB - // clientSecret: null - // issuer: null - // tokenEndpoint: >- - // https://sdx-authz-apps-gov-bc-ca-lab.apps.gov.bc.ca/auth/realms/sdx/protocol/openid-connect/token - // clientPublicKey: null - // clientPrivateKey: null - // const serviceAccess = await getOpenAccessRequestsByConsumer( // ctx, // ns, From 036ecfbc1907bfebaea04d6799668c817b42f31d Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Sat, 23 Aug 2025 00:24:18 -0700 Subject: [PATCH 037/109] add access request retrieval --- src/batch/data-rules.js | 2 + src/services/keystone/access-request.ts | 16 ++- src/services/report/data/consumer-requests.ts | 2 +- .../integrated/keystonejs/accessRequest.ts | 109 ++++++++++-------- 4 files changed, 76 insertions(+), 53 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 344b6f796..a1146cd9e 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -1,3 +1,5 @@ +const AccessRequest = require("../lists/AccessRequest"); + const metadata = { Organization: { query: 'allOrganizations', diff --git a/src/services/keystone/access-request.ts b/src/services/keystone/access-request.ts index ce4089ec2..6bb3c8690 100644 --- a/src/services/keystone/access-request.ts +++ b/src/services/keystone/access-request.ts @@ -4,6 +4,7 @@ import { AccessRequest, AccessRequestCreateInput, AccessRequestUpdateInput, + AccessRequestWhereInput, } from './types'; const assert = require('assert').strict; @@ -126,12 +127,15 @@ export async function getAccessRequest(context: any, id: string): Promise { const query = gql` - query GetNamespaceAccessRequests($ns: String!) { + query GetNamespaceAccessRequests($nsList: [String]!) { allAccessRequests( - where: { productEnvironment: { product: { namespace: $ns } } } + where: { OR: [ + { productEnvironment: { product: { namespace_in: $nsList } } }, + { application: { namespace_in: $nsList } } + ] } ) { id name @@ -145,6 +149,7 @@ export async function getAccessRequestsByNamespace( application { name appId + namespace } requestor { username @@ -154,6 +159,8 @@ export async function getAccessRequestsByNamespace( appId flow product { + namespace + openapiSpecs name } } @@ -161,6 +168,7 @@ export async function getAccessRequestsByNamespace( id consumer { username + tags } } createdAt @@ -168,7 +176,7 @@ export async function getAccessRequestsByNamespace( } `; - const result = await context.executeGraphQL({ query, variables: { ns } }); + const result = await context.executeGraphQL({ query, variables: { nsList } }); logger.debug('Query [getAccessRequestsByNamespace] result %j', result); return result.data.allAccessRequests; } diff --git a/src/services/report/data/consumer-requests.ts b/src/services/report/data/consumer-requests.ts index d0332e442..868d3d7c7 100644 --- a/src/services/report/data/consumer-requests.ts +++ b/src/services/report/data/consumer-requests.ts @@ -29,7 +29,7 @@ export async function getConsumerRequests( ): Promise { const dataPromises = namespaces.map( async (ns): Promise => { - const requests = await getAccessRequestsByNamespace(ksCtx, ns.name); + const requests = await getAccessRequestsByNamespace(ksCtx, [ ns.name ]); // services let data: ReportOfConsumerRequest[] = []; diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 2612a2a45..fdefc0958 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -36,6 +36,7 @@ import { addAccessRequest, collectCredentials, getAccessRequest, + getAccessRequestsByNamespace, } from '../../../services/keystone/access-request'; import { createApplication } from '../../../services/keystone/application'; import { @@ -66,54 +67,66 @@ import { authentication: { item: identity }, }); - // o(await getOrganizations(ctx)); - const app = await createApplication(ctx, { - name: 'App ' + new Date().toISOString(), - description: 'App Desc', - ownerId: userId, - }); - - const controls = { - clientName: app.name, - subjectDn: 'CN=my-site', - //defaultClientScopes: [], - optionalClientScopes: ['user/Test1'], - }; - - const accessRequestData = { - acceptLegal: false, - additionalDetails: 'here is some additional details', - controls: JSON.stringify(controls), - name: 'Sample API FOR Cope, Aidan CITZ:EX', - productEnvironmentId: '13', - requestor: userId, - } as any; - - accessRequestData.applicationId = app.id; - - const result = await addAccessRequest(ctx, accessRequestData); - o(result); - - const creds = await collectCredentials(ctx, result.id); - const credDetails = JSON.parse(creds.credential); - o(credDetails); - - const request = await getAccessRequest(ctx, result.id); - o(request); - - const labels = [ - { labelGroup: 'sdx-member', values: ['/MIN/CITZ'] }, - { labelGroup: 'sdx-res-locator', values: ['/LAB/MIN/CITZ/MYSVC-API'] }, - { labelGroup: "application", values: [app.name] } - ]; - - await saveConsumerLabels(ctx, ns, request.serviceAccess.consumer.id, labels); - - // const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); - // o(revoke); - - // const revoke = await deleteServiceAccess(ctx, request.serviceAccess.id); - // o(revoke); + if (false) { + // o(await getOrganizations(ctx)); + const app = await createApplication(ctx, { + name: 'App ' + new Date().toISOString(), + description: 'App Desc', + ownerId: userId, + }); + + const controls = { + clientName: app.name, + subjectDn: 'CN=my-site', + //defaultClientScopes: [], + optionalClientScopes: ['user/Test1'], + }; + + const accessRequestData = { + acceptLegal: false, + additionalDetails: 'here is some additional details', + controls: JSON.stringify(controls), + name: 'Sample API FOR Cope, Aidan CITZ:EX', + productEnvironmentId: '13', + requestor: userId, + } as any; + + accessRequestData.applicationId = app.id; + + const result = await addAccessRequest(ctx, accessRequestData); + o(result); + + const creds = await collectCredentials(ctx, result.id); + const credDetails = JSON.parse(creds.credential); + o(credDetails); + + const request = await getAccessRequest(ctx, result.id); + o(request); + + const labels = [ + { labelGroup: 'sdx-member', values: ['/MIN/CITZ'] }, + { labelGroup: 'sdx-res-locator', values: ['/LAB/MIN/CITZ/MYSVC-API'] }, + { labelGroup: 'application', values: [app.name] }, + ]; + + await saveConsumerLabels( + ctx, + ns, + request.serviceAccess.consumer.id, + labels + ); + + // const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); + // o(revoke); + + // const revoke = await deleteServiceAccess(ctx, request.serviceAccess.id); + // o(revoke); + } + + if (true) { + const result = await getAccessRequestsByNamespace(ctx, [ns]); + o(result); + } // const serviceAccess = await getOpenAccessRequestsByConsumer( // ctx, From 8ccf39d15dd85687f8eff8b9ab701c7b364105ac Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Sat, 23 Aug 2025 00:42:31 -0700 Subject: [PATCH 038/109] add org access requests apis --- .../v3/OrgAccessRequestsController.ts | 114 ++++++++ src/controllers/v3/openapi.yaml | 252 +++++++++++------- src/controllers/v3/routes.ts | 170 ++++++++---- 3 files changed, 389 insertions(+), 147 deletions(-) create mode 100644 src/controllers/v3/OrgAccessRequestsController.ts diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts new file mode 100644 index 000000000..e9adb28ce --- /dev/null +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -0,0 +1,114 @@ +import { + Controller, + Request, + OperationId, + Put, + Path, + Route, + Security, + Body, + Get, + Tags, + Delete, +} from 'tsoa'; +import { strict as assert } from 'assert'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecordsThrowErrors, + getRecords, + parseJsonString, + removeEmpty, + removeKeys, + transformAllRefID, + deleteRecord, + replaceKey, +} from '../../batch/feed-worker'; +import { BatchResult } from '../../batch/types'; +import { Dataset, DraftDataset } from './types'; +import { Product } from './types'; + +@injectable() +@Route('/organizations') +@Tags('API Directory (Administration)') +export class OrgAccessRequestsController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Get Access Requests that are available by API for this organization + * > `Required Scope:` Namespace.Assign + * + * @summary Get Organization Access Requests + */ + @Get('/{org}/access_requests') + @OperationId('organization-access-requests') + @Security('jwt', ['Namespace.Assign']) + public async getRequests( + @Path() org: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + + // get list of namespaces for this org + // then get access requests for those namespaces + // const batchClause = { + // query: '$org: String', + // clause: '{ organization: { name: $org } }', + // variables: { org }, + // }; + + // const records = await getRecords( + // ctx, + // 'Product', + // undefined, + // ['environments'], + // batchClause + // ); + + // return records + // .map((o) => removeEmpty(o)) + // .map((o) => transformAllRefID(o, ['organization', 'organizationUnit'])) + // .map((o) => + // removeKeys(o, [ + // 'id' + // ]) + // ); + return []; + } + + + /** + * Manage Access Requests for APIs that will appear on the API Directory + * > `Required Scope:` Namespace.Assign + * + * @summary Manage Access Requests + * @param ns + * @param body + * @param request + */ + @Put('/{org}/gateways/{gatewayId}/access_requests') + @OperationId('organization-put-access-requests') + @Security('jwt', ['Namespace.Assign']) + public async put( + @Path() gatewayId: string, + @Path() org: string, + @Body() body: Product, + @Request() request: any + ): Promise { + // TODO: Make sure namespace is allowed for this org + // body['gatewayId'] = gatewayId; + // body['organization'] = org; + + // return await syncRecordsThrowErrors( + // this.keystone.createContext(request, true), + // 'Product', + // body['appId'], + // replaceKey(body, 'gatewayId', 'namespace') + // ); + return { status: 400, result: 'Not implemented'} + } +} diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 01d0026f2..39503afdb 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -380,6 +380,100 @@ components: mode: auto environmentDetails: [] owner: janis@gov.bc.ca + DraftDatasetRefID: + type: string + LegalRefID: + type: string + BlobRefID: + type: string + CredentialIssuerRefID: + type: string + Environment: + properties: + appId: + type: string + name: + type: string + enum: + - dev + - test + - prod + - sandbox + - other + active: + type: boolean + approval: + type: boolean + flow: + type: string + enum: + - public + - protected-externally + - authorization-code + - client-credentials + - kong-acl-only + - kong-api-key-only + - kong-api-key-acl + additionalDetailsToRequest: + type: string + services: + items: + $ref: '#/components/schemas/GatewayServiceRefID' + type: array + legal: + $ref: '#/components/schemas/LegalRefID' + spec: + $ref: '#/components/schemas/BlobRefID' + credentialIssuer: + $ref: '#/components/schemas/CredentialIssuerRefID' + type: object + additionalProperties: false + example: + name: dev + active: false + approval: false + flow: public + appId: '00000000' + Product: + properties: + appId: + type: string + name: + type: string + type: + type: string + enum: + - app + - service + description: + type: string + gatewayId: + type: string + openapiSpecs: + items: + type: string + type: array + dataset: + $ref: '#/components/schemas/DraftDatasetRefID' + environments: + items: + $ref: '#/components/schemas/Environment' + type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + type: object + additionalProperties: false + example: + name: my-new-product + appId: '000000000000' + type: service + environments: + - + name: dev + active: false + approval: false + flow: public + appId: '00000000' OrganizationUnit: properties: extForeignKey: @@ -532,100 +626,6 @@ components: type: string type: object additionalProperties: false - DraftDatasetRefID: - type: string - LegalRefID: - type: string - BlobRefID: - type: string - CredentialIssuerRefID: - type: string - Environment: - properties: - appId: - type: string - name: - type: string - enum: - - dev - - test - - prod - - sandbox - - other - active: - type: boolean - approval: - type: boolean - flow: - type: string - enum: - - public - - protected-externally - - authorization-code - - client-credentials - - kong-acl-only - - kong-api-key-only - - kong-api-key-acl - additionalDetailsToRequest: - type: string - services: - items: - $ref: '#/components/schemas/GatewayServiceRefID' - type: array - legal: - $ref: '#/components/schemas/LegalRefID' - spec: - $ref: '#/components/schemas/BlobRefID' - credentialIssuer: - $ref: '#/components/schemas/CredentialIssuerRefID' - type: object - additionalProperties: false - example: - name: dev - active: false - approval: false - flow: public - appId: '00000000' - Product: - properties: - appId: - type: string - name: - type: string - type: - type: string - enum: - - app - - service - description: - type: string - gatewayId: - type: string - openapiSpecs: - items: - type: string - type: array - dataset: - $ref: '#/components/schemas/DraftDatasetRefID' - environments: - items: - $ref: '#/components/schemas/Environment' - type: array - organization: - $ref: '#/components/schemas/OrganizationRefID' - type: object - additionalProperties: false - example: - name: my-new-product - appId: '000000000000' - type: service - environments: - - - name: dev - active: false - approval: false - flow: public - appId: '00000000' securitySchemes: jwt: type: oauth2 @@ -1305,6 +1305,70 @@ paths: required: true schema: type: string + '/organizations/{org}/access_requests': + get: + operationId: organization-access-requests + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Dataset' + type: array + description: "Get Access Requests that are available by API for this organization\n> `Required Scope:` Namespace.Assign" + summary: 'Get Organization Access Requests' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + '/organizations/{org}/gateways/{gatewayId}/access_requests': + put: + operationId: organization-put-access-requests + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Manage Access Requests for APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Assign" + summary: 'Manage Access Requests' + tags: + - 'API Directory (Administration)' + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Product' /organizations: get: operationId: organization-list diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 083b1932f..14e634337 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -21,6 +21,8 @@ import { IdentifiersController } from './IdentifierController'; // 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 { IssuerController } from './IssuerController'; // 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 { OrgAccessRequestsController } from './OrgAccessRequestsController'; +// 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 { OrganizationController } from './OrganizationController'; // 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 { OrgProductController } from './OrgProductController'; @@ -250,6 +252,59 @@ 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 + "DraftDatasetRefID": { + "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 + "LegalRefID": { + "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 + "BlobRefID": { + "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 + "CredentialIssuerRefID": { + "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 + "Environment": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, + "active": {"dataType":"boolean"}, + "approval": {"dataType":"boolean"}, + "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, + "additionalDetailsToRequest": {"dataType":"string"}, + "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, + "legal": {"ref":"LegalRefID"}, + "spec": {"ref":"BlobRefID"}, + "credentialIssuer": {"ref":"CredentialIssuerRefID"}, + }, + "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 + "Product": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"string"}, + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["app"]},{"dataType":"enum","enums":["service"]}]}, + "description": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, + "dataset": {"ref":"DraftDatasetRefID"}, + "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + "organization": {"ref":"OrganizationRefID"}, + }, + "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 "OrganizationUnit": { "dataType": "refObject", "properties": { @@ -360,59 +415,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 - "DraftDatasetRefID": { - "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 - "LegalRefID": { - "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 - "BlobRefID": { - "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 - "CredentialIssuerRefID": { - "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 - "Environment": { - "dataType": "refObject", - "properties": { - "appId": {"dataType":"string"}, - "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, - "active": {"dataType":"boolean"}, - "approval": {"dataType":"boolean"}, - "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, - "additionalDetailsToRequest": {"dataType":"string"}, - "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, - "legal": {"ref":"LegalRefID"}, - "spec": {"ref":"BlobRefID"}, - "credentialIssuer": {"ref":"CredentialIssuerRefID"}, - }, - "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 - "Product": { - "dataType": "refObject", - "properties": { - "appId": {"dataType":"string"}, - "name": {"dataType":"string"}, - "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["app"]},{"dataType":"enum","enums":["service"]}]}, - "description": {"dataType":"string"}, - "gatewayId": {"dataType":"string"}, - "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, - "dataset": {"ref":"DraftDatasetRefID"}, - "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, - "organization": {"ref":"OrganizationRefID"}, - }, - "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 }; const validationService = new ValidationService(models); @@ -1146,6 +1148,68 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/:org/access_requests', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrgAccessRequestsController_getRequests(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + 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(OrgAccessRequestsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getRequests.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/v3/organizations/:org/gateways/:gatewayId/access_requests', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrgAccessRequestsController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Product"}, + 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(OrgAccessRequestsController); + 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/v3/organizations', async function OrganizationController_listOrganizations(request: any, response: any, next: any) { From 88395ddc2abe54295f535a8b9295e4454f9f3b06 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 12:53:17 -0700 Subject: [PATCH 039/109] upd test --- src/test/integrated/keystonejs/accessRequest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index fdefc0958..20f772775 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -67,10 +67,10 @@ import { authentication: { item: identity }, }); - if (false) { + if (true) { // o(await getOrganizations(ctx)); const app = await createApplication(ctx, { - name: 'App ' + new Date().toISOString(), + name: 'App y ' + new Date().toISOString(), description: 'App Desc', ownerId: userId, }); @@ -86,7 +86,7 @@ import { acceptLegal: false, additionalDetails: 'here is some additional details', controls: JSON.stringify(controls), - name: 'Sample API FOR Cope, Aidan CITZ:EX', + name: 'Sampler API FOR Cope, Aidan CITZ:EX', productEnvironmentId: '13', requestor: userId, } as any; @@ -123,7 +123,7 @@ import { // o(revoke); } - if (true) { + if (false) { const result = await getAccessRequestsByNamespace(ctx, [ns]); o(result); } From 9d81c124b43f403d830205feae314e2d3b84aa7e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 13:49:36 -0700 Subject: [PATCH 040/109] add to org access requests api --- src/batch/data-rules.js | 1 - .../v3/OrgAccessRequestsController.ts | 32 +++++-------- src/controllers/v3/types-extra.ts | 35 +++++++++++++++ src/services/keystone/application.ts | 24 ++++++++-- src/services/workflow/get-namespaces.ts | 13 ++++++ .../integrated/keystonejs/accessRequest.ts | 45 ++++++++++++++++++- 6 files changed, 123 insertions(+), 27 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index a1146cd9e..a9fca2957 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -1,4 +1,3 @@ -const AccessRequest = require("../lists/AccessRequest"); const metadata = { Organization: { diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index e9adb28ce..305b7638e 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -27,6 +27,10 @@ import { import { BatchResult } from '../../batch/types'; import { Dataset, DraftDataset } from './types'; import { Product } from './types'; +import { getGwaProductEnvironment } from '@/services/workflow'; +import { getOrgNamespaces } from '@/services/workflow/get-namespaces'; +import { getAccessRequestsByNamespace } from '@/services/keystone'; +import { OrgAccessRequest } from './types-extra'; @injectable() @Route('/organizations') @@ -50,34 +54,21 @@ export class OrgAccessRequestsController extends Controller { public async getRequests( @Path() org: string, @Request() request: any - ): Promise { + ): Promise { const ctx = this.keystone.createContext(request); - // get list of namespaces for this org - // then get access requests for those namespaces - // const batchClause = { - // query: '$org: String', - // clause: '{ organization: { name: $org } }', - // variables: { org }, - // }; + const prodEnv = await getGwaProductEnvironment(ctx, false); - // const records = await getRecords( - // ctx, - // 'Product', - // undefined, - // ['environments'], - // batchClause - // ); + const nsList = await getOrgNamespaces(org, prodEnv); + const records = await getAccessRequestsByNamespace(ctx, nsList.map((n) => n.name)); // return records // .map((o) => removeEmpty(o)) // .map((o) => transformAllRefID(o, ['organization', 'organizationUnit'])) // .map((o) => - // removeKeys(o, [ - // 'id' - // ]) + // replaceKey(o, 'gatewayId', 'namespace') // ); - return []; + return records as any } @@ -90,11 +81,10 @@ export class OrgAccessRequestsController extends Controller { * @param body * @param request */ - @Put('/{org}/gateways/{gatewayId}/access_requests') + @Put('/{org}/access_requests') @OperationId('organization-put-access-requests') @Security('jwt', ['Namespace.Assign']) public async put( - @Path() gatewayId: string, @Path() org: string, @Body() body: Product, @Request() request: any diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 8ee99974d..b7ba04d07 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -26,4 +26,39 @@ export interface GatewayAdd { org?: string; domains?: string; dataPlane?: string; +} + +export interface OrgAccessRequest { + id: string; + name: string; + isApproved: boolean; + isIssued: boolean; + isComplete: boolean; + requestor: { + name: string; + username: string; + }; + application: { + name: string; + appId: string; + namespace: string; + }; + productEnvironment: { + name: string; + appId: string; + flow: string; + product: { + namespace: string; + openapiSpecs: string[]; + name: string; + }; + }; + serviceAccess: { + id: string; + consumer: { + username: string; + tags: string[]; + }; + }; + createdAt: Scalars['DateTime']; } \ No newline at end of file diff --git a/src/services/keystone/application.ts b/src/services/keystone/application.ts index 8e15a6849..abab60d5b 100644 --- a/src/services/keystone/application.ts +++ b/src/services/keystone/application.ts @@ -42,15 +42,33 @@ export async function lookupMyApplicationsById( return result.data.myApplications[0]; } +export async function lookupApplicationByNamespaces( + context: any, + namespaces: string[] +): Promise { + const result = await context.executeGraphQL({ + query: `query GetApplicationByNamespaces($namespaces: [String!]) { + allApplications(where: {namespace_in: $namespaces}) { + id + appId + name + } + }`, + variables: { namespaces }, + }); + logger.debug('[lookupApplicationByNamespaces] result %j', result); + return result.data.allApplications; +} + export async function createApplication( context: any, - data: { name: string, ownerId: string, description?: string } + data: { name: string, ownerId: string, description?: string, namespace?: string } ): Promise { logger.debug('[createApplication] %j', data); const result = await context.executeGraphQL({ - query: `mutation CreateApplication($name: String!, $description: String, $ownerId: ID!) { - createApplication(data: {name: $name, owner: {connect: {id: $ownerId}}, description: $description}) { + query: `mutation CreateApplication($name: String!, $description: String, $ownerId: ID!, $namespace: String) { + createApplication(data: {name: $name, owner: {connect: {id: $ownerId}}, description: $description, namespace: $namespace}) { id appId name diff --git a/src/services/workflow/get-namespaces.ts b/src/services/workflow/get-namespaces.ts index 4132462b9..b4e9fcb02 100644 --- a/src/services/workflow/get-namespaces.ts +++ b/src/services/workflow/get-namespaces.ts @@ -31,6 +31,8 @@ import { import getSubjectToken from '../../auth/auth-token'; import { Logger } from '../../logger'; +import { NamespaceService } from '../org-groups'; +import { OrgNamespace } from '../org-groups/types'; const logger = Logger('wf.getns'); @@ -46,6 +48,17 @@ export async function getGwaProductEnvironment( return getEnvironmentContext(context, prodEnvId, {}, withSubject); } +export async function getOrgNamespaces( + org: string, + envCtx: EnvironmentContext +): Promise { + const envConfig = envCtx.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + return svc.listAssignedNamespacesByOrg(org); +} + export async function getMyNamespaces( envCtx: EnvironmentContext ): Promise { diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 20f772775..5c7070dc4 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -38,11 +38,19 @@ import { getAccessRequest, getAccessRequestsByNamespace, } from '../../../services/keystone/access-request'; -import { createApplication } from '../../../services/keystone/application'; +import { + createApplication, + lookupApplicationByNamespaces, +} from '../../../services/keystone/application'; import { revokeAllConsumerAccess, saveConsumerLabels, } from '../../../services/workflow'; +import { + getGwaProductEnvironment, + getOrgNamespaces, +} from '../../../services/workflow/get-namespaces'; +import { getRecords } from '../../../batch/feed-worker'; (async () => { const keystone = await InitKeystone(); @@ -67,12 +75,13 @@ import { authentication: { item: identity }, }); - if (true) { + if (false) { // o(await getOrganizations(ctx)); const app = await createApplication(ctx, { name: 'App y ' + new Date().toISOString(), description: 'App Desc', ownerId: userId, + namespace: ns, }); const controls = { @@ -123,6 +132,38 @@ import { // o(revoke); } + if (true) { + const org = 'ministry-of-citizens-services'; + const prodEnv = await getGwaProductEnvironment(ctx, false); + + const nsList = await getOrgNamespaces(org, prodEnv); + o(nsList); + + const apps = await lookupApplicationByNamespaces(ctx, [ns]); + o(apps); + + const result = await getAccessRequestsByNamespace( + ctx, + nsList.map((n) => n.name) + ); + o(result); + + const batchClause = { + query: '$org: String', + clause: '{ organization: { name: $org } }', + variables: { org }, + }; + + const records = await getRecords( + ctx, + 'Product', + undefined, + ['environments'], + batchClause + ); + o(records); + } + if (false) { const result = await getAccessRequestsByNamespace(ctx, [ns]); o(result); From 32a31c099545693cea7bb3d0b71666e26a212a59 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 14:47:31 -0700 Subject: [PATCH 041/109] cleanup org access req --- .../v3/OrgAccessRequestsController.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 305b7638e..286a14936 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -9,27 +9,14 @@ import { Body, Get, Tags, - Delete, } from 'tsoa'; -import { strict as assert } from 'assert'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; -import { - syncRecordsThrowErrors, - getRecords, - parseJsonString, - removeEmpty, - removeKeys, - transformAllRefID, - deleteRecord, - replaceKey, -} from '../../batch/feed-worker'; import { BatchResult } from '../../batch/types'; -import { Dataset, DraftDataset } from './types'; import { Product } from './types'; -import { getGwaProductEnvironment } from '@/services/workflow'; -import { getOrgNamespaces } from '@/services/workflow/get-namespaces'; -import { getAccessRequestsByNamespace } from '@/services/keystone'; +import { getGwaProductEnvironment } from '../../services/workflow'; +import { getOrgNamespaces } from '../../services/workflow/get-namespaces'; +import { getAccessRequestsByNamespace } from '../../services/keystone'; import { OrgAccessRequest } from './types-extra'; @injectable() From fcffda5e41b8f14bd87dc6142833d38b36901c11 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 17:20:22 -0700 Subject: [PATCH 042/109] upd org access request --- .../v3/OrgAccessRequestsController.ts | 2 +- src/services/keystone/application.ts | 10 +- src/services/keystone/product-environment.ts | 1 + src/services/keystone/types.ts | 1 + src/services/workflow/org-access-request.ts | 176 ++++++++++++++++++ .../integrated/keystonejs/accessRequest.ts | 57 +++--- 6 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 src/services/workflow/org-access-request.ts diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 286a14936..b3b0087ed 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -42,7 +42,7 @@ export class OrgAccessRequestsController extends Controller { @Path() org: string, @Request() request: any ): Promise { - const ctx = this.keystone.createContext(request); + const ctx = this.keystone.createContext(request, true); const prodEnv = await getGwaProductEnvironment(ctx, false); diff --git a/src/services/keystone/application.ts b/src/services/keystone/application.ts index abab60d5b..1dd778e38 100644 --- a/src/services/keystone/application.ts +++ b/src/services/keystone/application.ts @@ -42,10 +42,10 @@ export async function lookupMyApplicationsById( return result.data.myApplications[0]; } -export async function lookupApplicationByNamespaces( +export async function lookupApplicationsByNamespaces( context: any, namespaces: string[] -): Promise { +): Promise { const result = await context.executeGraphQL({ query: `query GetApplicationByNamespaces($namespaces: [String!]) { allApplications(where: {namespace_in: $namespaces}) { @@ -63,12 +63,12 @@ export async function lookupApplicationByNamespaces( export async function createApplication( context: any, - data: { name: string, ownerId: string, description?: string, namespace?: string } + data: { appId?: string, name: string, ownerId: string, description?: string, namespace?: string } ): Promise { logger.debug('[createApplication] %j', data); const result = await context.executeGraphQL({ - query: `mutation CreateApplication($name: String!, $description: String, $ownerId: ID!, $namespace: String) { - createApplication(data: {name: $name, owner: {connect: {id: $ownerId}}, description: $description, namespace: $namespace}) { + query: `mutation CreateApplication($appId: String, $name: String!, $description: String, $ownerId: ID!, $namespace: String) { + createApplication(data: {appId: $appId, name: $name, owner: {connect: {id: $ownerId}}, description: $description, namespace: $namespace}) { id appId name diff --git a/src/services/keystone/product-environment.ts b/src/services/keystone/product-environment.ts index e710d0744..7fd95afd2 100644 --- a/src/services/keystone/product-environment.ts +++ b/src/services/keystone/product-environment.ts @@ -85,6 +85,7 @@ export async function lookupProductEnvironmentServicesBySlug( name flow product { + name namespace } credentialIssuer { diff --git a/src/services/keystone/types.ts b/src/services/keystone/types.ts index 5d5de8201..435ec3e75 100644 --- a/src/services/keystone/types.ts +++ b/src/services/keystone/types.ts @@ -758,6 +758,7 @@ export type Application = { id: Scalars['ID']; appId?: Maybe; name?: Maybe; + namespace?: Maybe; description?: Maybe; certificate?: Maybe; organization?: Maybe; diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts new file mode 100644 index 000000000..56013b63a --- /dev/null +++ b/src/services/workflow/org-access-request.ts @@ -0,0 +1,176 @@ +import { assert } from 'console'; +import { Logger } from '../../logger'; +import { lookupCredentialIssuerById, lookupEnvironmentAndIssuerById, lookupProduct, lookupProductEnvironmentServicesBySlug } from '../keystone'; +import { + addAccessRequest, + collectCredentials, + getAccessRequest, + getAccessRequestsByNamespace, +} from '../keystone/access-request'; +import { + createApplication, + lookupApplicationsByNamespaces, +} from '../keystone/application'; +import { AccessRequest, Application, Environment } from '../keystone/types'; +import { saveConsumerLabels } from './consumer-management'; +import { getGwaProductEnvironment, getOrgNamespaces } from './get-namespaces'; +import { NewCredential } from './types'; + +const logger = Logger('wf.OrgAccessReq'); + +export const OrgAccessRequest = async ( + context: any, + org: string, + orgMemberID: string, + userId: string, + consumerProdEnvAppId: string, + providerProdEnvAppId: string, + businessProcess: string, + accessPointDN: string, + optionalClientScopes: string[], +): Promise<{ + application: Application, + providerProdEnv: Environment, +accessRequest: AccessRequest, +credential: NewCredential, + }> => { + // get list of namespaces for this org + const prodEnv = await getGwaProductEnvironment(context, false); + const nsList = await getOrgNamespaces(org, prodEnv); + + // get the consumer product environment details + const consumerProdEnv = await lookupProductEnvironmentServicesBySlug( + context, + consumerProdEnvAppId + ); + + assert(nsList.filter(ns => ns.name === consumerProdEnv.product.namespace).length === 1, `Consumer Product Environment ${consumerProdEnvAppId} not found`); + + // create the application if it does not exist + const app = { + appId: `sdx${consumerProdEnv.appId}`, + name: `${consumerProdEnv.product.name} ${consumerProdEnv.name}`, + description: '', + owner: { id: userId }, + namespace: consumerProdEnv.product.namespace, + } as Application; + + const appId = await UpsertApplication(context, app); + logger.debug('App ID: %s', appId); + + // get the provider product environment details + const providerProdEnv = await lookupProductEnvironmentServicesBySlug( + context, + providerProdEnvAppId + ); + + // get the provider credential issuer details + const providerCredIssuer = await lookupCredentialIssuerById(context, providerProdEnv.credentialIssuer.id); + providerProdEnv.credentialIssuer = providerCredIssuer; + + // prepare the access request + const controls = { + clientName: app.name, + subjectDn: accessPointDN, + //defaultClientScopes: [], + optionalClientScopes, + }; + + const accessRequestData = { + acceptLegal: false, + additionalDetails: 'here is some additional details', + controls: JSON.stringify(controls), + name: 'Sampler API FOR Cope, Aidan CITZ:EX', + applicationId: appId, + productEnvironmentId: providerProdEnv.id, + requestor: userId, + } as any; + + // create the access request + const accessRequestCreated = await addAccessRequest( + context, + accessRequestData + ); + + // collect the credentials + const creds = await collectCredentials(context, accessRequestCreated.id); + const credDetails = JSON.parse(creds.credential); + + // get the latest details of the access request + const accessRequest = await getAccessRequest( + context, + accessRequestCreated.id + ); + + // add some standard labels to the consumer + const labels = [ + { labelGroup: 'sdx-res-locator', values: [formatResourceLocator(orgMemberID, consumerProdEnv)] }, + { labelGroup: 'sdx-member', values: [orgMemberID] }, + ]; + + if (businessProcess) { + labels.push({ labelGroup: 'purpose', values: [businessProcess] }); + } + + await saveConsumerLabels( + context, + app.namespace, + accessRequest.serviceAccess.consumer.id, + labels + ); + + return { + application: app, + providerProdEnv, + accessRequest, + credential: credDetails, + }; +}; + +const UpsertApplication = async ( + context: any, + application: Application +): Promise => { + const ns = application.namespace; + const apps = await lookupApplicationsByNamespaces(context, [ns]); + if (apps.filter((a) => a.appId === application.appId).length > 0) { + logger.debug(`Application ${application.appId} already exists`); + return apps.find((a) => a.appId === application.appId).id; + } else { + const app = await createApplication(context, { + appId: application.appId, + name: application.name, + description: application.description, + ownerId: application.owner?.id, + namespace: application.namespace, + }); + return app.id; + } +}; + +const checkAccessRequestExists = async ( + context: any, + namespace: string, + applicationId: string, + productEnvironmentId: string +): Promise => { + const accessRequests = await getAccessRequestsByNamespace(context, [ + namespace, + ]); + return ( + accessRequests.filter( + (ar) => + ar.application.id === applicationId && + ar.productEnvironment.id === productEnvironmentId + ).length > 0 + ); +}; + +const formatResourceLocator = ( + orgMemberID: string,providerProdEnv: Environment, +): string => { + const env = providerProdEnv.name.toUpperCase(); + const serviceId = providerProdEnv.product.name; + + return `/${env}/${orgMemberID}/${serviceId}`; +} \ No newline at end of file diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 5c7070dc4..f19837f3d 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -40,7 +40,7 @@ import { } from '../../../services/keystone/access-request'; import { createApplication, - lookupApplicationByNamespaces, + lookupApplicationsByNamespaces, } from '../../../services/keystone/application'; import { revokeAllConsumerAccess, @@ -50,12 +50,13 @@ import { getGwaProductEnvironment, getOrgNamespaces, } from '../../../services/workflow/get-namespaces'; -import { getRecords } from '../../../batch/feed-worker'; +import { getRecords, replaceKey } from '../../../batch/feed-worker'; +import { OrgAccessRequest } from '../../../services/workflow/org-access-request'; (async () => { const keystone = await InitKeystone(); - const ns = 'gw-0a524'; + const ns = 'gw-84b1f'; const skipAccessControl = true; const userId = '12'; @@ -75,6 +76,14 @@ import { getRecords } from '../../../batch/feed-worker'; authentication: { item: identity }, }); + if (true) { + // 424C7EB5 SDX-WORKING-API (dev) + // 38D0FED9 SDX-SAMPLE-API (prod) + // + const result = await OrgAccessRequest(ctx, 'ministry-of-citizens-services', 'MIN/CITZ', userId, '424C7EB5', '7A031F2A', "SDX Onboarding", "CN=abcd", ["user/Test2"]); + o(result); + } + if (false) { // o(await getOrganizations(ctx)); const app = await createApplication(ctx, { @@ -132,39 +141,43 @@ import { getRecords } from '../../../batch/feed-worker'; // o(revoke); } - if (true) { + if (false) { const org = 'ministry-of-citizens-services'; const prodEnv = await getGwaProductEnvironment(ctx, false); const nsList = await getOrgNamespaces(org, prodEnv); o(nsList); - const apps = await lookupApplicationByNamespaces(ctx, [ns]); + const apps = await lookupApplicationsByNamespaces(ctx, [ns]); o(apps); const result = await getAccessRequestsByNamespace( ctx, nsList.map((n) => n.name) ); - o(result); - - const batchClause = { - query: '$org: String', - clause: '{ organization: { name: $org } }', - variables: { org }, - }; - - const records = await getRecords( - ctx, - 'Product', - undefined, - ['environments'], - batchClause - ); - o(records); + const recs = result + .map((o) => + replaceKey(o, 'gatewayId', 'namespace') + ); + o(recs); + + // const batchClause = { + // query: '$org: String', + // clause: '{ organization: { name: $org } }', + // variables: { org }, + // }; + + // const records = await getRecords( + // ctx, + // 'Product', + // undefined, + // ['environments'], + // batchClause + // ); + // o(records); } - if (false) { + if (true) { const result = await getAccessRequestsByNamespace(ctx, [ns]); o(result); } From 0a86dbd7a77e7a4e1e95617e7f24867ffe9bdadd Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 22:30:05 -0700 Subject: [PATCH 043/109] add org create access request --- .../v3/OrgAccessRequestsController.ts | 24 +++++++++---------- src/controllers/v3/types-extra.ts | 9 +++++++ src/services/workflow/org-access-request.ts | 4 ++-- .../integrated/keystonejs/accessRequest.ts | 5 ++-- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index b3b0087ed..08dc1bdf1 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -17,7 +17,8 @@ import { Product } from './types'; import { getGwaProductEnvironment } from '../../services/workflow'; import { getOrgNamespaces } from '../../services/workflow/get-namespaces'; import { getAccessRequestsByNamespace } from '../../services/keystone'; -import { OrgAccessRequest } from './types-extra'; +import { OrgAccessRequest, OrgAccessRequestCreateInput } from './types-extra'; +import { OrgAccessRequestCreate } from '../../services/workflow/org-access-request'; @injectable() @Route('/organizations') @@ -73,19 +74,16 @@ export class OrgAccessRequestsController extends Controller { @Security('jwt', ['Namespace.Assign']) public async put( @Path() org: string, - @Body() body: Product, + @Body() body: OrgAccessRequestCreateInput, @Request() request: any - ): Promise { - // TODO: Make sure namespace is allowed for this org - // body['gatewayId'] = gatewayId; - // body['organization'] = org; + ): Promise { + const ctx = this.keystone.createContext(request, true); + + const userId = ctx['user']['id']; + + const result = await OrgAccessRequestCreate(ctx, org, body.orgMemberId, userId, + body.consumerProductEnvAppId, body.providerProductEnvAppId, body.businessProcess, body.accessPointDN, body.optionalClientScopes); - // return await syncRecordsThrowErrors( - // this.keystone.createContext(request, true), - // 'Product', - // body['appId'], - // replaceKey(body, 'gatewayId', 'namespace') - // ); - return { status: 400, result: 'Not implemented'} + return result.accessRequest as any } } diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index b7ba04d07..5362a8496 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -28,6 +28,15 @@ export interface GatewayAdd { dataPlane?: string; } +export interface OrgAccessRequestCreateInput { + orgMemberId: string; + consumerProductEnvAppId: string; + providerProductEnvAppId: string; + businessProcess: string; + accessPointDN: string; + optionalClientScopes: string[]; +} + export interface OrgAccessRequest { id: string; name: string; diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index 56013b63a..a43fd27c4 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -18,7 +18,7 @@ import { NewCredential } from './types'; const logger = Logger('wf.OrgAccessReq'); -export const OrgAccessRequest = async ( +export const OrgAccessRequestCreate = async ( context: any, org: string, orgMemberID: string, @@ -70,7 +70,7 @@ credential: NewCredential, // prepare the access request const controls = { - clientName: app.name, + clientName: `${formatResourceLocator(orgMemberID, consumerProdEnv)} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`, subjectDn: accessPointDN, //defaultClientScopes: [], optionalClientScopes, diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index f19837f3d..4057321bf 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -51,7 +51,7 @@ import { getOrgNamespaces, } from '../../../services/workflow/get-namespaces'; import { getRecords, replaceKey } from '../../../batch/feed-worker'; -import { OrgAccessRequest } from '../../../services/workflow/org-access-request'; +import { OrgAccessRequestCreate } from '../../../services/workflow/org-access-request'; (async () => { const keystone = await InitKeystone(); @@ -80,7 +80,8 @@ import { OrgAccessRequest } from '../../../services/workflow/org-access-request' // 424C7EB5 SDX-WORKING-API (dev) // 38D0FED9 SDX-SAMPLE-API (prod) // - const result = await OrgAccessRequest(ctx, 'ministry-of-citizens-services', 'MIN/CITZ', userId, '424C7EB5', '7A031F2A', "SDX Onboarding", "CN=abcd", ["user/Test2"]); + const result = await OrgAccessRequestCreate(ctx, 'ministry-of-citizens-services', 'MIN/CITZ', userId, + '424C7EB5', '7A031F2A', "SDX Onboarding", "CN=abcd", ["user/Test2"]); o(result); } From 9974069d7820e16a65cf132044d7c86b9d62a72b Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 22:51:27 -0700 Subject: [PATCH 044/109] try getting userid another way --- src/controllers/v3/OrgAccessRequestsController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 08dc1bdf1..aa2188fa1 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -79,7 +79,8 @@ export class OrgAccessRequestsController extends Controller { ): Promise { const ctx = this.keystone.createContext(request, true); - const userId = ctx['user']['id']; + + const userId = ctx.authedItem.userId const result = await OrgAccessRequestCreate(ctx, org, body.orgMemberId, userId, body.consumerProductEnvAppId, body.providerProductEnvAppId, body.businessProcess, body.accessPointDN, body.optionalClientScopes); From d36ab3f5dab83abfba66655bdd451d82228d3d66 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 23:08:16 -0700 Subject: [PATCH 045/109] try getting userid another way --- src/controllers/v3/OrgAccessRequestsController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index aa2188fa1..d9d26fb97 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -80,7 +80,9 @@ export class OrgAccessRequestsController extends Controller { const ctx = this.keystone.createContext(request, true); - const userId = ctx.authedItem.userId + const userId = body.userId; + + //const userId = ctx.authedItem.userId const result = await OrgAccessRequestCreate(ctx, org, body.orgMemberId, userId, body.consumerProductEnvAppId, body.providerProductEnvAppId, body.businessProcess, body.accessPointDN, body.optionalClientScopes); From 6218ca97f51e29da31900148fb919ca45875d63c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 29 Aug 2025 23:17:36 -0700 Subject: [PATCH 046/109] try getting userid another way --- src/controllers/v3/types-extra.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 5362a8496..6aa3e09d8 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -30,6 +30,7 @@ export interface GatewayAdd { export interface OrgAccessRequestCreateInput { orgMemberId: string; + userId: string; consumerProductEnvAppId: string; providerProductEnvAppId: string; businessProcess: string; From 6197732185e945872580bd9593e168e3f4b12d84 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 14:22:15 -0700 Subject: [PATCH 047/109] add org access request creation --- .../v3/OrgAccessRequestsController.ts | 44 ++- src/controllers/v3/openapi.yaml | 287 ++++++++++++------ src/controllers/v3/routes.ts | 118 ++++--- src/lists/extensions/OrgAccessRequest.ts | 87 ++++++ src/services/keystone/access-request.ts | 8 + src/services/workflow/org-access-request.ts | 45 ++- src/services/workflow/types.ts | 11 + .../integrated/keystonejs/accessRequest.ts | 83 ++++- src/test/integrated/keystonejs/init.ts | 1 + 9 files changed, 522 insertions(+), 162 deletions(-) create mode 100644 src/lists/extensions/OrgAccessRequest.ts diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index d9d26fb97..5c3cfc6bb 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -9,6 +9,8 @@ import { Body, Get, Tags, + FieldErrors, + ValidateError, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; @@ -19,6 +21,11 @@ import { getOrgNamespaces } from '../../services/workflow/get-namespaces'; import { getAccessRequestsByNamespace } from '../../services/keystone'; import { OrgAccessRequest, OrgAccessRequestCreateInput } from './types-extra'; import { OrgAccessRequestCreate } from '../../services/workflow/org-access-request'; +import { Logger } from '../../logger'; +import { gql } from 'graphql-request'; +import { data } from 'msw/lib/types/context'; + +const logger = Logger('controllers.OrgAccessReq'); @injectable() @Route('/organizations') @@ -76,17 +83,34 @@ export class OrgAccessRequestsController extends Controller { @Path() org: string, @Body() body: OrgAccessRequestCreateInput, @Request() request: any - ): Promise { - const ctx = this.keystone.createContext(request, true); - - - const userId = body.userId; - - //const userId = ctx.authedItem.userId + ): Promise<{id: string}> { - const result = await OrgAccessRequestCreate(ctx, org, body.orgMemberId, userId, - body.consumerProductEnvAppId, body.providerProductEnvAppId, body.businessProcess, body.accessPointDN, body.optionalClientScopes); + const result = await this.keystone.executeGraphQL({ + context: this.keystone.createContext(request), + query: createAccessRequest, + variables: { data: body }, + }); + logger.debug('Result %j', result); + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + logger.error('%j', result); + throw new ValidateError(errors, 'Unable to create Gateway'); + } + return { + id: result.data.orgAccessRequest.id, + }; - return result.accessRequest as any } } + +const createAccessRequest = gql` + mutation OrgAccessRequestCreate($data: OrgAccessRequestCreateInput!) { + orgAccessRequest(data: $data) { + id + } + } +`; + diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 39503afdb..3ebb024d6 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -380,100 +380,130 @@ components: mode: auto environmentDetails: [] owner: janis@gov.bc.ca - DraftDatasetRefID: - type: string - LegalRefID: - type: string - BlobRefID: - type: string - CredentialIssuerRefID: - type: string - Environment: + OrgAccessRequest: properties: - appId: + id: type: string name: type: string - enum: - - dev - - test - - prod - - sandbox - - other - active: + isApproved: type: boolean - approval: + isIssued: type: boolean - flow: - type: string - enum: - - public - - protected-externally - - authorization-code - - client-credentials - - kong-acl-only - - kong-api-key-only - - kong-api-key-acl - additionalDetailsToRequest: - type: string - services: - items: - $ref: '#/components/schemas/GatewayServiceRefID' - type: array - legal: - $ref: '#/components/schemas/LegalRefID' - spec: - $ref: '#/components/schemas/BlobRefID' - credentialIssuer: - $ref: '#/components/schemas/CredentialIssuerRefID' + isComplete: + type: boolean + requestor: + properties: + username: + type: string + name: + type: string + required: + - username + - name + type: object + application: + properties: + namespace: + type: string + appId: + type: string + name: + type: string + required: + - namespace + - appId + - name + type: object + productEnvironment: + properties: + product: + properties: + name: + type: string + openapiSpecs: + items: {type: string} + type: array + namespace: + type: string + required: + - name + - openapiSpecs + - namespace + type: object + flow: + type: string + appId: + type: string + name: + type: string + required: + - product + - flow + - appId + - name + type: object + serviceAccess: + properties: + consumer: + properties: + tags: + items: {type: string} + type: array + username: + type: string + required: + - tags + - username + type: object + id: + type: string + required: + - consumer + - id + type: object + createdAt: {} + required: + - id + - name + - isApproved + - isIssued + - isComplete + - requestor + - application + - productEnvironment + - serviceAccess + - createdAt type: object additionalProperties: false - example: - name: dev - active: false - approval: false - flow: public - appId: '00000000' - Product: + OrgAccessRequestCreateInput: properties: - appId: + orgMemberId: type: string - name: + userId: type: string - type: + consumerProductEnvAppId: type: string - enum: - - app - - service - description: + providerProductEnvAppId: type: string - gatewayId: + businessProcess: type: string - openapiSpecs: + accessPointDN: + type: string + optionalClientScopes: items: type: string type: array - dataset: - $ref: '#/components/schemas/DraftDatasetRefID' - environments: - items: - $ref: '#/components/schemas/Environment' - type: array - organization: - $ref: '#/components/schemas/OrganizationRefID' + required: + - orgMemberId + - userId + - consumerProductEnvAppId + - providerProductEnvAppId + - businessProcess + - accessPointDN + - optionalClientScopes type: object additionalProperties: false - example: - name: my-new-product - appId: '000000000000' - type: service - environments: - - - name: dev - active: false - approval: false - flow: public - appId: '00000000' OrganizationUnit: properties: extForeignKey: @@ -626,6 +656,100 @@ components: type: string type: object additionalProperties: false + DraftDatasetRefID: + type: string + LegalRefID: + type: string + BlobRefID: + type: string + CredentialIssuerRefID: + type: string + Environment: + properties: + appId: + type: string + name: + type: string + enum: + - dev + - test + - prod + - sandbox + - other + active: + type: boolean + approval: + type: boolean + flow: + type: string + enum: + - public + - protected-externally + - authorization-code + - client-credentials + - kong-acl-only + - kong-api-key-only + - kong-api-key-acl + additionalDetailsToRequest: + type: string + services: + items: + $ref: '#/components/schemas/GatewayServiceRefID' + type: array + legal: + $ref: '#/components/schemas/LegalRefID' + spec: + $ref: '#/components/schemas/BlobRefID' + credentialIssuer: + $ref: '#/components/schemas/CredentialIssuerRefID' + type: object + additionalProperties: false + example: + name: dev + active: false + approval: false + flow: public + appId: '00000000' + Product: + properties: + appId: + type: string + name: + type: string + type: + type: string + enum: + - app + - service + description: + type: string + gatewayId: + type: string + openapiSpecs: + items: + type: string + type: array + dataset: + $ref: '#/components/schemas/DraftDatasetRefID' + environments: + items: + $ref: '#/components/schemas/Environment' + type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + type: object + additionalProperties: false + example: + name: my-new-product + appId: '000000000000' + type: service + environments: + - + name: dev + active: false + approval: false + flow: public + appId: '00000000' securitySchemes: jwt: type: oauth2 @@ -1315,7 +1439,7 @@ paths: application/json: schema: items: - $ref: '#/components/schemas/Dataset' + $ref: '#/components/schemas/OrgAccessRequest' type: array description: "Get Access Requests that are available by API for this organization\n> `Required Scope:` Namespace.Assign" summary: 'Get Organization Access Requests' @@ -1332,7 +1456,6 @@ paths: required: true schema: type: string - '/organizations/{org}/gateways/{gatewayId}/access_requests': put: operationId: organization-put-access-requests responses: @@ -1341,7 +1464,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BatchResult' + $ref: '#/components/schemas/OrgAccessRequest' description: "Manage Access Requests for APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Assign" summary: 'Manage Access Requests' tags: @@ -1351,12 +1474,6 @@ paths: jwt: - Namespace.Assign parameters: - - - in: path - name: gatewayId - required: true - schema: - type: string - in: path name: org @@ -1368,7 +1485,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Product' + $ref: '#/components/schemas/OrgAccessRequestCreateInput' /organizations: get: operationId: organization-list diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 14e634337..e7f58e7f0 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -252,55 +252,33 @@ 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 - "DraftDatasetRefID": { - "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 - "LegalRefID": { - "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 - "BlobRefID": { - "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 - "CredentialIssuerRefID": { - "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 - "Environment": { + "OrgAccessRequest": { "dataType": "refObject", "properties": { - "appId": {"dataType":"string"}, - "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, - "active": {"dataType":"boolean"}, - "approval": {"dataType":"boolean"}, - "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, - "additionalDetailsToRequest": {"dataType":"string"}, - "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, - "legal": {"ref":"LegalRefID"}, - "spec": {"ref":"BlobRefID"}, - "credentialIssuer": {"ref":"CredentialIssuerRefID"}, + "id": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "isApproved": {"dataType":"boolean","required":true}, + "isIssued": {"dataType":"boolean","required":true}, + "isComplete": {"dataType":"boolean","required":true}, + "requestor": {"dataType":"nestedObjectLiteral","nestedProperties":{"username":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, + "application": {"dataType":"nestedObjectLiteral","nestedProperties":{"namespace":{"dataType":"string","required":true},"appId":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, + "productEnvironment": {"dataType":"nestedObjectLiteral","nestedProperties":{"product":{"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"openapiSpecs":{"dataType":"array","array":{"dataType":"string"},"required":true},"namespace":{"dataType":"string","required":true}},"required":true},"flow":{"dataType":"string","required":true},"appId":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, + "serviceAccess": {"dataType":"nestedObjectLiteral","nestedProperties":{"consumer":{"dataType":"nestedObjectLiteral","nestedProperties":{"tags":{"dataType":"array","array":{"dataType":"string"},"required":true},"username":{"dataType":"string","required":true}},"required":true},"id":{"dataType":"string","required":true}},"required":true}, + "createdAt": {"dataType":"any","required":true}, }, "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 - "Product": { + "OrgAccessRequestCreateInput": { "dataType": "refObject", "properties": { - "appId": {"dataType":"string"}, - "name": {"dataType":"string"}, - "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["app"]},{"dataType":"enum","enums":["service"]}]}, - "description": {"dataType":"string"}, - "gatewayId": {"dataType":"string"}, - "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, - "dataset": {"ref":"DraftDatasetRefID"}, - "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, - "organization": {"ref":"OrganizationRefID"}, + "orgMemberId": {"dataType":"string","required":true}, + "userId": {"dataType":"string","required":true}, + "consumerProductEnvAppId": {"dataType":"string","required":true}, + "providerProductEnvAppId": {"dataType":"string","required":true}, + "businessProcess": {"dataType":"string","required":true}, + "accessPointDN": {"dataType":"string","required":true}, + "optionalClientScopes": {"dataType":"array","array":{"dataType":"string"},"required":true}, }, "additionalProperties": false, }, @@ -415,6 +393,59 @@ 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 + "DraftDatasetRefID": { + "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 + "LegalRefID": { + "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 + "BlobRefID": { + "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 + "CredentialIssuerRefID": { + "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 + "Environment": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, + "active": {"dataType":"boolean"}, + "approval": {"dataType":"boolean"}, + "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, + "additionalDetailsToRequest": {"dataType":"string"}, + "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, + "legal": {"ref":"LegalRefID"}, + "spec": {"ref":"BlobRefID"}, + "credentialIssuer": {"ref":"CredentialIssuerRefID"}, + }, + "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 + "Product": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"string"}, + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["app"]},{"dataType":"enum","enums":["service"]}]}, + "description": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "openapiSpecs": {"dataType":"array","array":{"dataType":"string"}}, + "dataset": {"ref":"DraftDatasetRefID"}, + "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + "organization": {"ref":"OrganizationRefID"}, + }, + "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 }; const validationService = new ValidationService(models); @@ -1178,14 +1209,13 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/:org/gateways/:gatewayId/access_requests', + app.put('/ds/api/v3/organizations/:org/access_requests', authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), async function OrgAccessRequestsController_put(request: any, response: any, next: any) { const args = { - gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, org: {"in":"path","name":"org","required":true,"dataType":"string"}, - body: {"in":"body","name":"body","required":true,"ref":"Product"}, + body: {"in":"body","name":"body","required":true,"ref":"OrgAccessRequestCreateInput"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, }; diff --git a/src/lists/extensions/OrgAccessRequest.ts b/src/lists/extensions/OrgAccessRequest.ts new file mode 100644 index 000000000..5ef916c08 --- /dev/null +++ b/src/lists/extensions/OrgAccessRequest.ts @@ -0,0 +1,87 @@ +const { EnforcementPoint } = require('../../authz/enforcement'); +import { UMAPolicyService, Policy, PolicyQuery } from '../../services/uma2'; +import { getEnvironmentContext, getResourceSets } from './Common'; +import { strict as assert } from 'assert'; +import { Logger } from '../../logger'; +import { StructuredActivityService } from '../../services/workflow'; +import { + createUmaPolicy, + updateUmaPolicy, + revokeUmaPolicy, +} from '../../services/workflow/ns-uma-policy-access'; +import PolicyRepresentation from '@keycloak/keycloak-admin-client/lib/defs/policyRepresentation'; +import { UmaPolicyInput } from '../../services/keystone/types'; +import { OrgAccessRequestCreateInput } from '../../controllers/v3/types-extra'; +import { OrgAccessRequestCreate } from '../..//services/workflow/org-access-request'; + +const logger = Logger('lists.orgaccessreq'); + +const typeOrgAccessRequestCreateInput = ` + input OrgAccessRequestCreateInput { + org: String!, + orgMemberId: String!, + userId: String!, + consumerProductEnvAppId: String!, + providerProductEnvAppId: String!, + businessProcess: String!, + accessPointDN: String!, + optionalClientScopes: [String!] + } +`; + +const typeOrgAccessRequest = ` + type OrgAccessRequest { + application: Application, + providerProdEnv: Environment, + accessRequest: AccessRequest, + } +`; + + + +module.exports = { + extensions: [ + (keystone: any) => { + keystone.extendGraphQLSchema({ + types: [{ type: typeOrgAccessRequestCreateInput }, { type: typeOrgAccessRequest}], + queries: [ + ], + mutations: [ + { + schema: + 'orgCreateAccessRequest(data: OrgAccessRequestCreateInput): OrgAccessRequest', + resolver: async ( + item: any, + args: any, + context: any, + info: any, + { query, access }: any + ) => { + const result = await OrgAccessRequestCreate( + context, + args.data.org, + args.data.orgMemberId, + args.data.userId, + args.data.consumerProductEnvAppId, + args.data.providerProductEnvAppId, + args.data.businessProcess, + args.data.accessPointDN, + args.data.optionalClientScopes || [], + ); + logger.debug('OrgCreateAccessRequest: %j', result); + return { + application: { + appId: result.accessRequest.application.appId, + }, + accessRequest: { + id: result.accessRequest.id, + } + }; + }, + access: EnforcementPoint, + } + ], + }); + }, + ], +}; diff --git a/src/services/keystone/access-request.ts b/src/services/keystone/access-request.ts index 6bb3c8690..2d717c76d 100644 --- a/src/services/keystone/access-request.ts +++ b/src/services/keystone/access-request.ts @@ -51,7 +51,15 @@ export async function addAccessRequest( query, variables: { ...data }, }); + logger.debug('Mutation [addAccessRequest] result %j', result); + + assert.strictEqual( + 'errors' in result, + false, + 'Error adding access request' + ); + return result.data.createAccessRequest; } diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index a43fd27c4..ba46c2ea5 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -1,6 +1,11 @@ import { assert } from 'console'; import { Logger } from '../../logger'; -import { lookupCredentialIssuerById, lookupEnvironmentAndIssuerById, lookupProduct, lookupProductEnvironmentServicesBySlug } from '../keystone'; +import { + lookupCredentialIssuerById, + lookupEnvironmentAndIssuerById, + lookupProduct, + lookupProductEnvironmentServicesBySlug, +} from '../keystone'; import { addAccessRequest, collectCredentials, @@ -27,13 +32,13 @@ export const OrgAccessRequestCreate = async ( providerProdEnvAppId: string, businessProcess: string, accessPointDN: string, - optionalClientScopes: string[], + optionalClientScopes: string[] ): Promise<{ - application: Application, - providerProdEnv: Environment, -accessRequest: AccessRequest, -credential: NewCredential, - }> => { + application: Application; + providerProdEnv: Environment; + accessRequest: AccessRequest; + credential: NewCredential; +}> => { // get list of namespaces for this org const prodEnv = await getGwaProductEnvironment(context, false); const nsList = await getOrgNamespaces(org, prodEnv); @@ -44,7 +49,11 @@ credential: NewCredential, consumerProdEnvAppId ); - assert(nsList.filter(ns => ns.name === consumerProdEnv.product.namespace).length === 1, `Consumer Product Environment ${consumerProdEnvAppId} not found`); + assert( + nsList.filter((ns) => ns.name === consumerProdEnv.product.namespace) + .length === 1, + `Consumer Product Environment ${consumerProdEnvAppId} not found` + ); // create the application if it does not exist const app = { @@ -65,12 +74,18 @@ credential: NewCredential, ); // get the provider credential issuer details - const providerCredIssuer = await lookupCredentialIssuerById(context, providerProdEnv.credentialIssuer.id); + const providerCredIssuer = await lookupCredentialIssuerById( + context, + providerProdEnv.credentialIssuer.id + ); providerProdEnv.credentialIssuer = providerCredIssuer; // prepare the access request const controls = { - clientName: `${formatResourceLocator(orgMemberID, consumerProdEnv)} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`, + clientName: `${formatResourceLocator( + orgMemberID, + consumerProdEnv + )} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`, subjectDn: accessPointDN, //defaultClientScopes: [], optionalClientScopes, @@ -104,7 +119,10 @@ credential: NewCredential, // add some standard labels to the consumer const labels = [ - { labelGroup: 'sdx-res-locator', values: [formatResourceLocator(orgMemberID, consumerProdEnv)] }, + { + labelGroup: 'sdx-res-locator', + values: [formatResourceLocator(orgMemberID, consumerProdEnv)], + }, { labelGroup: 'sdx-member', values: [orgMemberID] }, ]; @@ -167,10 +185,11 @@ const checkAccessRequestExists = async ( }; const formatResourceLocator = ( - orgMemberID: string,providerProdEnv: Environment, + orgMemberID: string, + providerProdEnv: Environment ): string => { const env = providerProdEnv.name.toUpperCase(); const serviceId = providerProdEnv.product.name; return `/${env}/${orgMemberID}/${serviceId}`; -} \ No newline at end of file +}; diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index e894bf25f..0998daed9 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -217,3 +217,14 @@ export interface ActivitySummary { activityAt: Scalars['DateTime']; blob?: any; } + +export interface OrgAccessRequestCreateInput { + org: string; + orgMemberId: string; + userId: string; + consumerProductEnvAppId: string; + providerProductEnvAppId: string; + businessProcess: string; + accessPointDN: string; + optionalClientScopes: string[]; +} \ No newline at end of file diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 4057321bf..7b1886181 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -52,11 +52,13 @@ import { } from '../../../services/workflow/get-namespaces'; import { getRecords, replaceKey } from '../../../batch/feed-worker'; import { OrgAccessRequestCreate } from '../../../services/workflow/org-access-request'; +import { OrgAccessRequestCreateInput } from '../../../services/workflow/types'; (async () => { const keystone = await InitKeystone(); - const ns = 'gw-84b1f'; +// const ns = 'gw-84b1f'; + const ns = 'gw-31a33'; const skipAccessControl = true; const userId = '12'; @@ -77,14 +79,78 @@ import { OrgAccessRequestCreate } from '../../../services/workflow/org-access-re }); if (true) { + const result = await ctx.executeGraphQL({ + query: ` + mutation OrgCreateAccessRequest ($data: OrgAccessRequestCreateInput) { + orgCreateAccessRequest (data: $data) { + application { + appId + } + accessRequest { + id + name + status + productEnvironment { + id + name + product { + id + name + namespace + } + } + application { + id + name + namespace + } + serviceAccess { + id + name + consumer { + id + name + namespace + } + } + } + } + } + `, + variables: { + data: { + org: 'ministry-of-puppies-and-kittens', + orgMemberId: 'MIN/PUKI', + userId, + consumerProductEnvAppId: 'E7FEB796', + providerProductEnvAppId: '1400BE49', + businessProcess: 'Vet Services', + accessPointDN: 'CN=sdx.gov.bc.ca', + optionalClientScopes: ['user/Test2'], + } as OrgAccessRequestCreateInput, + }, + }); + o(result); + } + + if (false) { // 424C7EB5 SDX-WORKING-API (dev) // 38D0FED9 SDX-SAMPLE-API (prod) - // - const result = await OrgAccessRequestCreate(ctx, 'ministry-of-citizens-services', 'MIN/CITZ', userId, - '424C7EB5', '7A031F2A', "SDX Onboarding", "CN=abcd", ["user/Test2"]); + // + const result = await OrgAccessRequestCreate( + ctx, + 'ministry-of-citizens-services', + 'MIN/CITZ', + userId, + '424C7EB5', + '7A031F2A', + 'SDX Onboarding', + 'CN=abcd', + ['user/Test2'] + ); o(result); } - + if (false) { // o(await getOrganizations(ctx)); const app = await createApplication(ctx, { @@ -156,10 +222,7 @@ import { OrgAccessRequestCreate } from '../../../services/workflow/org-access-re ctx, nsList.map((n) => n.name) ); - const recs = result - .map((o) => - replaceKey(o, 'gatewayId', 'namespace') - ); + const recs = result.map((o) => replaceKey(o, 'gatewayId', 'namespace')); o(recs); // const batchClause = { @@ -178,7 +241,7 @@ import { OrgAccessRequestCreate } from '../../../services/workflow/org-access-re // o(records); } - if (true) { + if (false) { const result = await getAccessRequestsByNamespace(ctx, [ns]); o(result); } diff --git a/src/test/integrated/keystonejs/init.ts b/src/test/integrated/keystonejs/init.ts index e69f7e11e..2555c401c 100644 --- a/src/test/integrated/keystonejs/init.ts +++ b/src/test/integrated/keystonejs/init.ts @@ -106,6 +106,7 @@ export default async function InitKeystone( 'CredentialIssuerExt', 'Namespace', 'NamespaceActivity', + 'OrgAccessRequest', 'OrganizationPolicy', 'ServiceAccess', 'ServiceAccount', From c3efdf4818ea18dcc431bdeac93ce6d017d36912 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 14:46:45 -0700 Subject: [PATCH 048/109] upd org access request --- .../v3/OrgAccessRequestsController.ts | 42 ++++++++++++++++--- src/controllers/v3/types-extra.ts | 1 + src/lists/extensions/OrgAccessRequest.ts | 26 ++++-------- src/services/workflow/org-access-request.ts | 23 ++++++---- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 5c3cfc6bb..ef64c311c 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -11,19 +11,22 @@ import { Tags, FieldErrors, ValidateError, + Delete, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; import { BatchResult } from '../../batch/types'; import { Product } from './types'; -import { getGwaProductEnvironment } from '../../services/workflow'; +import { getGwaProductEnvironment, revokeAllConsumerAccess } from '../../services/workflow'; import { getOrgNamespaces } from '../../services/workflow/get-namespaces'; -import { getAccessRequestsByNamespace } from '../../services/keystone'; +import { getAccessRequestByNamespaceServiceAccess, getAccessRequestsByNamespace } from '../../services/keystone'; import { OrgAccessRequest, OrgAccessRequestCreateInput } from './types-extra'; import { OrgAccessRequestCreate } from '../../services/workflow/org-access-request'; import { Logger } from '../../logger'; import { gql } from 'graphql-request'; import { data } from 'msw/lib/types/context'; +import { getAccessRequest } from '@/services/keystone/access-request'; +import { assert } from 'console'; const logger = Logger('controllers.OrgAccessReq'); @@ -67,6 +70,32 @@ export class OrgAccessRequestsController extends Controller { } + /** Delete Access Request + * > `Required Scope:` Namespace.Assign + */ + @Delete('/{org}/access_requests/{id}') + @OperationId('organization-delete-access-request') + @Security('jwt', ['Namespace.Assign']) + public async deleteRequest( + @Path() org: string, + @Path() id: string, + @Request() request: any + ): Promise<{}> { + const ctx = this.keystone.createContext(request, true); + + const prodEnv = await getGwaProductEnvironment(ctx, false); + + const accessRequest = await getAccessRequest(ctx, id); + + const ns = accessRequest.productEnvironment.product.namespace; + + const revoke = await revokeAllConsumerAccess(ctx, ns, accessRequest.serviceAccess.id); + logger.debug('Revoke Result %j', revoke); + + return {}; + } + + /** * Manage Access Requests for APIs that will appear on the API Directory * > `Required Scope:` Namespace.Assign @@ -85,6 +114,8 @@ export class OrgAccessRequestsController extends Controller { @Request() request: any ): Promise<{id: string}> { + body.org = org; + const result = await this.keystone.executeGraphQL({ context: this.keystone.createContext(request), query: createAccessRequest, @@ -97,7 +128,7 @@ export class OrgAccessRequestsController extends Controller { errors[`d${ind}`] = { message: err.message }; }); logger.error('%j', result); - throw new ValidateError(errors, 'Unable to create Gateway'); + throw new ValidateError(errors, 'Unable to create Access Request'); } return { id: result.data.orgAccessRequest.id, @@ -107,8 +138,9 @@ export class OrgAccessRequestsController extends Controller { } const createAccessRequest = gql` - mutation OrgAccessRequestCreate($data: OrgAccessRequestCreateInput!) { - orgAccessRequest(data: $data) { + + mutation OrgAccessRequestCreate ($data: OrgAccessRequestCreateInput!) { + orgAccessRequest (data: $data) { id } } diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 6aa3e09d8..91c1efde9 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -29,6 +29,7 @@ export interface GatewayAdd { } export interface OrgAccessRequestCreateInput { + org: string; orgMemberId: string; userId: string; consumerProductEnvAppId: string; diff --git a/src/lists/extensions/OrgAccessRequest.ts b/src/lists/extensions/OrgAccessRequest.ts index 5ef916c08..0751b555f 100644 --- a/src/lists/extensions/OrgAccessRequest.ts +++ b/src/lists/extensions/OrgAccessRequest.ts @@ -1,17 +1,5 @@ const { EnforcementPoint } = require('../../authz/enforcement'); -import { UMAPolicyService, Policy, PolicyQuery } from '../../services/uma2'; -import { getEnvironmentContext, getResourceSets } from './Common'; -import { strict as assert } from 'assert'; import { Logger } from '../../logger'; -import { StructuredActivityService } from '../../services/workflow'; -import { - createUmaPolicy, - updateUmaPolicy, - revokeUmaPolicy, -} from '../../services/workflow/ns-uma-policy-access'; -import PolicyRepresentation from '@keycloak/keycloak-admin-client/lib/defs/policyRepresentation'; -import { UmaPolicyInput } from '../../services/keystone/types'; -import { OrgAccessRequestCreateInput } from '../../controllers/v3/types-extra'; import { OrgAccessRequestCreate } from '../..//services/workflow/org-access-request'; const logger = Logger('lists.orgaccessreq'); @@ -37,15 +25,15 @@ const typeOrgAccessRequest = ` } `; - - module.exports = { extensions: [ (keystone: any) => { keystone.extendGraphQLSchema({ - types: [{ type: typeOrgAccessRequestCreateInput }, { type: typeOrgAccessRequest}], - queries: [ + types: [ + { type: typeOrgAccessRequestCreateInput }, + { type: typeOrgAccessRequest }, ], + queries: [], mutations: [ { schema: @@ -66,7 +54,7 @@ module.exports = { args.data.providerProductEnvAppId, args.data.businessProcess, args.data.accessPointDN, - args.data.optionalClientScopes || [], + args.data.optionalClientScopes || [] ); logger.debug('OrgCreateAccessRequest: %j', result); return { @@ -75,11 +63,11 @@ module.exports = { }, accessRequest: { id: result.accessRequest.id, - } + }, }; }, access: EnforcementPoint, - } + }, ], }); }, diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index ba46c2ea5..f321d8512 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -59,7 +59,10 @@ export const OrgAccessRequestCreate = async ( const app = { appId: `sdx${consumerProdEnv.appId}`, name: `${consumerProdEnv.product.name} ${consumerProdEnv.name}`, - description: '', + description: `SDX Resource Locator: ${formatResourceLocator( + orgMemberID, + consumerProdEnv + )} (Gateway ID ${consumerProdEnv.product.namespace})`, owner: { id: userId }, namespace: consumerProdEnv.product.namespace, } as Application; @@ -80,12 +83,14 @@ export const OrgAccessRequestCreate = async ( ); providerProdEnv.credentialIssuer = providerCredIssuer; - // prepare the access request - const controls = { - clientName: `${formatResourceLocator( + const clientName = `${formatResourceLocator( orgMemberID, consumerProdEnv - )} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`, + )} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`; + + // prepare the access request + const controls = { + clientName, subjectDn: accessPointDN, //defaultClientScopes: [], optionalClientScopes, @@ -95,7 +100,7 @@ export const OrgAccessRequestCreate = async ( acceptLegal: false, additionalDetails: 'here is some additional details', controls: JSON.stringify(controls), - name: 'Sampler API FOR Cope, Aidan CITZ:EX', + name: clientName, applicationId: appId, productEnvironmentId: providerProdEnv.id, requestor: userId, @@ -186,10 +191,10 @@ const checkAccessRequestExists = async ( const formatResourceLocator = ( orgMemberID: string, - providerProdEnv: Environment + serviceProdEnv: Environment ): string => { - const env = providerProdEnv.name.toUpperCase(); - const serviceId = providerProdEnv.product.name; + const env = serviceProdEnv.name.toUpperCase(); + const serviceId = serviceProdEnv.product.name; return `/${env}/${orgMemberID}/${serviceId}`; }; From 499efc422e899faaaf33e938bf026b82f06ba6c5 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 15:16:56 -0700 Subject: [PATCH 049/109] fix errored controller --- src/controllers/v3/OrgAccessRequestsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index ef64c311c..fe935d252 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -25,7 +25,7 @@ import { OrgAccessRequestCreate } from '../../services/workflow/org-access-reque import { Logger } from '../../logger'; import { gql } from 'graphql-request'; import { data } from 'msw/lib/types/context'; -import { getAccessRequest } from '@/services/keystone/access-request'; +import { getAccessRequest } from '../../services/keystone/access-request'; import { assert } from 'console'; const logger = Logger('controllers.OrgAccessReq'); From c317b98c292271d35e331bf22f3f8c7f7b4b0404 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 15:52:24 -0700 Subject: [PATCH 050/109] fix org access request err --- src/server.ts | 1 + src/services/workflow/org-access-request.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index f1815a867..63bbd19d8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -176,6 +176,7 @@ for (const _list of [ 'CredentialIssuerExt', 'Namespace', 'NamespaceActivity', + 'OrgAccessRequest', 'OrganizationPolicy', 'ServiceAccess', 'ServiceAccount', diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index f321d8512..b165a20f2 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -1,4 +1,5 @@ -import { assert } from 'console'; + +import { strict as assert } from 'assert'; import { Logger } from '../../logger'; import { lookupCredentialIssuerById, @@ -39,6 +40,7 @@ export const OrgAccessRequestCreate = async ( accessRequest: AccessRequest; credential: NewCredential; }> => { + // get list of namespaces for this org const prodEnv = await getGwaProductEnvironment(context, false); const nsList = await getOrgNamespaces(org, prodEnv); From 726dd9489acc82961a669f2ed6525164c2fe3d7c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 19:40:38 -0700 Subject: [PATCH 051/109] adj create access req --- src/controllers/v3/OrgAccessRequestsController.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index fe935d252..27ea9c705 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -141,7 +141,12 @@ const createAccessRequest = gql` mutation OrgAccessRequestCreate ($data: OrgAccessRequestCreateInput!) { orgAccessRequest (data: $data) { - id + application { + appId + } + accessRequest { + id + } } } `; From 04c71f3839e324fcdb05de902bf90abdd5c54493 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 20:30:42 -0700 Subject: [PATCH 052/109] try fix issue with access request save --- .../v3/OrgAccessRequestsController.ts | 2 - src/services/workflow/org-access-request.ts | 215 +++++++++--------- 2 files changed, 109 insertions(+), 108 deletions(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 27ea9c705..3b8de7dcc 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -83,8 +83,6 @@ export class OrgAccessRequestsController extends Controller { ): Promise<{}> { const ctx = this.keystone.createContext(request, true); - const prodEnv = await getGwaProductEnvironment(ctx, false); - const accessRequest = await getAccessRequest(ctx, id); const ns = accessRequest.productEnvironment.product.namespace; diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index b165a20f2..2e1544302 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -1,4 +1,3 @@ - import { strict as assert } from 'assert'; import { Logger } from '../../logger'; import { @@ -40,116 +39,120 @@ export const OrgAccessRequestCreate = async ( accessRequest: AccessRequest; credential: NewCredential; }> => { - - // get list of namespaces for this org - const prodEnv = await getGwaProductEnvironment(context, false); - const nsList = await getOrgNamespaces(org, prodEnv); - - // get the consumer product environment details - const consumerProdEnv = await lookupProductEnvironmentServicesBySlug( - context, - consumerProdEnvAppId - ); - - assert( - nsList.filter((ns) => ns.name === consumerProdEnv.product.namespace) - .length === 1, - `Consumer Product Environment ${consumerProdEnvAppId} not found` - ); - - // create the application if it does not exist - const app = { - appId: `sdx${consumerProdEnv.appId}`, - name: `${consumerProdEnv.product.name} ${consumerProdEnv.name}`, - description: `SDX Resource Locator: ${formatResourceLocator( - orgMemberID, - consumerProdEnv - )} (Gateway ID ${consumerProdEnv.product.namespace})`, - owner: { id: userId }, - namespace: consumerProdEnv.product.namespace, - } as Application; - - const appId = await UpsertApplication(context, app); - logger.debug('App ID: %s', appId); - - // get the provider product environment details - const providerProdEnv = await lookupProductEnvironmentServicesBySlug( - context, - providerProdEnvAppId - ); - - // get the provider credential issuer details - const providerCredIssuer = await lookupCredentialIssuerById( - context, - providerProdEnv.credentialIssuer.id - ); - providerProdEnv.credentialIssuer = providerCredIssuer; - - const clientName = `${formatResourceLocator( + try { + // get list of namespaces for this org + const prodEnv = await getGwaProductEnvironment(context, false); + const nsList = await getOrgNamespaces(org, prodEnv); + + // get the consumer product environment details + const consumerProdEnv = await lookupProductEnvironmentServicesBySlug( + context, + consumerProdEnvAppId + ); + + assert( + nsList.filter((ns) => ns.name === consumerProdEnv.product.namespace) + .length === 1, + `Consumer Product Environment ${consumerProdEnvAppId} not found` + ); + + // create the application if it does not exist + const app = { + appId: `sdx${consumerProdEnv.appId}`, + name: `${consumerProdEnv.product.name} ${consumerProdEnv.name}`, + description: `SDX Resource Locator: ${formatResourceLocator( + orgMemberID, + consumerProdEnv + )} (Gateway ID ${consumerProdEnv.product.namespace})`, + owner: { id: userId }, + namespace: consumerProdEnv.product.namespace, + } as Application; + + const appId = await UpsertApplication(context, app); + logger.debug('App ID: %s', appId); + + // get the provider product environment details + const providerProdEnv = await lookupProductEnvironmentServicesBySlug( + context, + providerProdEnvAppId + ); + + // get the provider credential issuer details + const providerCredIssuer = await lookupCredentialIssuerById( + context, + providerProdEnv.credentialIssuer.id + ); + providerProdEnv.credentialIssuer = providerCredIssuer; + + const clientName = `${formatResourceLocator( orgMemberID, consumerProdEnv )} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`; - // prepare the access request - const controls = { - clientName, - subjectDn: accessPointDN, - //defaultClientScopes: [], - optionalClientScopes, - }; - - const accessRequestData = { - acceptLegal: false, - additionalDetails: 'here is some additional details', - controls: JSON.stringify(controls), - name: clientName, - applicationId: appId, - productEnvironmentId: providerProdEnv.id, - requestor: userId, - } as any; - - // create the access request - const accessRequestCreated = await addAccessRequest( - context, - accessRequestData - ); - - // collect the credentials - const creds = await collectCredentials(context, accessRequestCreated.id); - const credDetails = JSON.parse(creds.credential); - - // get the latest details of the access request - const accessRequest = await getAccessRequest( - context, - accessRequestCreated.id - ); - - // add some standard labels to the consumer - const labels = [ - { - labelGroup: 'sdx-res-locator', - values: [formatResourceLocator(orgMemberID, consumerProdEnv)], - }, - { labelGroup: 'sdx-member', values: [orgMemberID] }, - ]; - - if (businessProcess) { - labels.push({ labelGroup: 'purpose', values: [businessProcess] }); + // prepare the access request + const controls = { + clientName, + subjectDn: accessPointDN, + //defaultClientScopes: [], + optionalClientScopes, + }; + + const accessRequestData = { + acceptLegal: false, + additionalDetails: 'here is some additional details', + controls: JSON.stringify(controls), + name: clientName, + applicationId: appId, + productEnvironmentId: providerProdEnv.id, + requestor: userId, + } as any; + + // create the access request + const accessRequestCreated = await addAccessRequest( + context, + accessRequestData + ); + + // collect the credentials + const creds = await collectCredentials(context, accessRequestCreated.id); + const credDetails = JSON.parse(creds.credential); + + // get the latest details of the access request + const accessRequest = await getAccessRequest( + context, + accessRequestCreated.id + ); + + // add some standard labels to the consumer + const labels = [ + { + labelGroup: 'sdx-res-locator', + values: [formatResourceLocator(orgMemberID, consumerProdEnv)], + }, + { labelGroup: 'sdx-member', values: [orgMemberID] }, + ]; + + if (businessProcess) { + labels.push({ labelGroup: 'purpose', values: [businessProcess] }); + } + + await saveConsumerLabels( + context, + app.namespace, + accessRequest.serviceAccess.consumer.id, + labels + ); + + return { + application: app, + providerProdEnv, + accessRequest, + credential: credDetails, + }; + } catch (error) { + logger.error('OrgAccessRequestCreate error: %s', error?.message || error); + throw error; } - - await saveConsumerLabels( - context, - app.namespace, - accessRequest.serviceAccess.consumer.id, - labels - ); - - return { - application: app, - providerProdEnv, - accessRequest, - credential: credDetails, - }; }; const UpsertApplication = async ( From dc0dc3bc71f64fa20243fdf694deada620bae166 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 21:13:36 -0700 Subject: [PATCH 053/109] tweak org access request api --- src/controllers/v3/OrgAccessRequestsController.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 3b8de7dcc..237eda2d2 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -136,9 +136,8 @@ export class OrgAccessRequestsController extends Controller { } const createAccessRequest = gql` - - mutation OrgAccessRequestCreate ($data: OrgAccessRequestCreateInput!) { - orgAccessRequest (data: $data) { + mutation OrgAccessRequestCreate ($data: OrgAccessRequestCreateInput) { + orgCreateAccessRequest (data: $data) { application { appId } From bd8219dbaf6c0eb6363acd183ff004893007c1fd Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 21:38:17 -0700 Subject: [PATCH 054/109] skip access control for creating access request --- src/lists/extensions/OrgAccessRequest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lists/extensions/OrgAccessRequest.ts b/src/lists/extensions/OrgAccessRequest.ts index 0751b555f..eea996168 100644 --- a/src/lists/extensions/OrgAccessRequest.ts +++ b/src/lists/extensions/OrgAccessRequest.ts @@ -45,8 +45,9 @@ module.exports = { info: any, { query, access }: any ) => { + const noauthContext = context.createContext({ skipAccessControl: true }); const result = await OrgAccessRequestCreate( - context, + noauthContext, args.data.org, args.data.orgMemberId, args.data.userId, From 141b36c29451178f2072e39b5bc69d16bef2402e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 23:20:36 -0700 Subject: [PATCH 055/109] change delete org access to service access --- src/controllers/v3/OrgAccessRequestsController.ts | 6 +++--- src/controllers/v3/types-extra.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 237eda2d2..cec5da55b 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -19,7 +19,7 @@ import { BatchResult } from '../../batch/types'; import { Product } from './types'; import { getGwaProductEnvironment, revokeAllConsumerAccess } from '../../services/workflow'; import { getOrgNamespaces } from '../../services/workflow/get-namespaces'; -import { getAccessRequestByNamespaceServiceAccess, getAccessRequestsByNamespace } from '../../services/keystone'; +import { deleteServiceAccess, getAccessRequestByNamespaceServiceAccess, getAccessRequestsByNamespace } from '../../services/keystone'; import { OrgAccessRequest, OrgAccessRequestCreateInput } from './types-extra'; import { OrgAccessRequestCreate } from '../../services/workflow/org-access-request'; import { Logger } from '../../logger'; @@ -85,9 +85,9 @@ export class OrgAccessRequestsController extends Controller { const accessRequest = await getAccessRequest(ctx, id); - const ns = accessRequest.productEnvironment.product.namespace; + //const ns = accessRequest.productEnvironment.product.namespace; - const revoke = await revokeAllConsumerAccess(ctx, ns, accessRequest.serviceAccess.id); + const revoke = await deleteServiceAccess(ctx, accessRequest.serviceAccess.id); logger.debug('Revoke Result %j', revoke); return {}; diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 91c1efde9..678084775 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -29,7 +29,7 @@ export interface GatewayAdd { } export interface OrgAccessRequestCreateInput { - org: string; + org?: string; orgMemberId: string; userId: string; consumerProductEnvAppId: string; From 925eeda160d6202745961c0a4eb02968d686fff8 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 2 Sep 2025 23:34:44 -0700 Subject: [PATCH 056/109] include more gateway information --- src/services/keycloak/namespace-details.ts | 19 +++++++++++++------ src/services/org-groups/namespace.ts | 12 ++++++++++++ src/services/org-groups/types.ts | 2 ++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/services/keycloak/namespace-details.ts b/src/services/keycloak/namespace-details.ts index dc5e4d609..c32dbf917 100644 --- a/src/services/keycloak/namespace-details.ts +++ b/src/services/keycloak/namespace-details.ts @@ -17,6 +17,7 @@ import { } from '../../lists/extensions/Common'; import { GWAService } from '../gwaapi'; import { strict as assert } from 'assert'; +import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; const logger = Logger('kc.nsdetails'); @@ -79,6 +80,18 @@ export async function backfillGroupAttributes( assert.strictEqual(Boolean(nsPermissions), true, 'Invalid namespace'); + return backfillGroupRepAttributes(nsPermissions, detail, defaultSettings); + +} + +export async function backfillGroupRepAttributes( + nsPermissions: GroupRepresentation, + detail: any, + defaultSettings: any, +): Promise { + + assert.strictEqual(Boolean(nsPermissions), true, 'Invalid namespace'); + transformSingleValueAttributes(nsPermissions.attributes, [ 'description', 'perm-data-plane', @@ -90,12 +103,6 @@ export async function backfillGroupAttributes( 'org-updated-at', ]); - logger.debug( - '[backfillGroupAttributes] %s attributes %j', - ns, - nsPermissions.attributes - ); - const merged = { ...detail, ...defaultSettings, diff --git a/src/services/org-groups/namespace.ts b/src/services/org-groups/namespace.ts index d60d22a39..1fb0b087d 100644 --- a/src/services/org-groups/namespace.ts +++ b/src/services/org-groups/namespace.ts @@ -161,6 +161,12 @@ export class NamespaceService { .map((group) => ({ name: group.name, orgUnit: 'org-unit' in group.attributes ? group.attributes['org-unit'][0] : null, + permDataPlane: + 'perm-data-plane' in group.attributes + ? group.attributes['perm-data-plane'].pop() : '', + permDomains: + 'perm-domains' in group.attributes + ? group.attributes['perm-domains'] : [], enabled: 'org-enabled' in group.attributes ? group.attributes['org-enabled'][0] === 'true' @@ -181,6 +187,12 @@ export class NamespaceService { return { name: nsGroup.attributes['org'].pop(), orgUnit: nsGroup.attributes['org-unit'].pop(), + permDataPlane: + 'perm-data-plane' in nsGroup.attributes + ? nsGroup.attributes['perm-data-plane'] : '', + permDomains: + 'perm-domains' in nsGroup.attributes + ? nsGroup.attributes['perm-domains'] : [], enabled: 'org-enabled' in nsGroup.attributes ? nsGroup.attributes['org-enabled'][0] === 'true' diff --git a/src/services/org-groups/types.ts b/src/services/org-groups/types.ts index 6eba1af0f..3f71423e0 100644 --- a/src/services/org-groups/types.ts +++ b/src/services/org-groups/types.ts @@ -33,5 +33,7 @@ export interface OrgNamespace { name: string; orgUnit: string; enabled: boolean; + permDataPlane: string; + permDomains: string[]; updatedAt: number; } From b0f0b783301907827d04bf15fb43ca6fa3c525c0 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Sep 2025 09:21:16 -0700 Subject: [PATCH 057/109] upd perm for org access req creation --- src/controllers/v3/OrgAccessRequestsController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index cec5da55b..f4830cab3 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -111,11 +111,12 @@ export class OrgAccessRequestsController extends Controller { @Body() body: OrgAccessRequestCreateInput, @Request() request: any ): Promise<{id: string}> { + const ctx = this.keystone.createContext(request, true); body.org = org; const result = await this.keystone.executeGraphQL({ - context: this.keystone.createContext(request), + context: ctx, query: createAccessRequest, variables: { data: body }, }); From a34491a6a566271faf1724787f528de1cc21d18c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 09:20:59 -0700 Subject: [PATCH 058/109] have some tsoa be user called only --- src/controllers/ioc/keystoneInjector.ts | 44 +++++++++++++++++++ .../v3/OrgAccessRequestsController.ts | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/controllers/ioc/keystoneInjector.ts b/src/controllers/ioc/keystoneInjector.ts index 1da31cf8d..7b621fb33 100644 --- a/src/controllers/ioc/keystoneInjector.ts +++ b/src/controllers/ioc/keystoneInjector.ts @@ -2,6 +2,7 @@ import { Keystone } from '@keystonejs/keystone'; import { injectable } from 'tsyringe'; import { scopes, scopesToRoles } from '../../auth/scope-role-utils'; import { Logger } from '../../logger'; +import { lookupUserByUsername } from '../../services/keystone'; const logger = Logger('controller'); @@ -38,6 +39,49 @@ export class KeystoneService { return this.keystone.createContext({ skipAccessControl: true }); } + public async createContextithUser( + request: any, + skipAccessControl: boolean = false + ) { + const _scopes = scopes(request.user.scope); + + const identityProvider = request.user.identity_provider; + + if (!request.user && !request.user.preferred_username) { + throw new Error( + 'User information is required to create context with user' + ); + } + const tmpCtx = this.keystone.createContext({ + skipAccessControl: true, + }); + const users = await lookupUserByUsername( + tmpCtx, + request.user.preferred_username + ); + if (!users) { + throw new Error(`User ${request.user.preferred_username} not found`); + } + const userId = users[0].id; + + const identity = { + id: null, + name: resolveName(request.user), + username: resolveUsername(request.user), + namespace: request.params.ns || request.params.gatewayId, + roles: JSON.stringify(scopesToRoles(identityProvider, _scopes)), + scopes: _scopes, + userId, + } as any; + logger.debug('identity %j', identity); + const ctx = this.keystone.createContext({ + skipAccessControl, + authentication: { item: identity }, + }); + ctx.req = request; + return ctx; + } + public createContext(request: any, skipAccessControl: boolean = false): any { const _scopes = scopes(request.user.scope); diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index f4830cab3..3845ee237 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -111,7 +111,7 @@ export class OrgAccessRequestsController extends Controller { @Body() body: OrgAccessRequestCreateInput, @Request() request: any ): Promise<{id: string}> { - const ctx = this.keystone.createContext(request, true); + const ctx = await this.keystone.createContextithUser(request, true); body.org = org; From 8d184258ed588bf8d4cda12ceae01e11fe6e2141 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 10:00:38 -0700 Subject: [PATCH 059/109] temp disable validation --- src/services/workflow/validate-access-request.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/services/workflow/validate-access-request.ts b/src/services/workflow/validate-access-request.ts index ab92f5291..06a849d61 100644 --- a/src/services/workflow/validate-access-request.ts +++ b/src/services/workflow/validate-access-request.ts @@ -90,12 +90,15 @@ export const Validate = async ( ); // assert that either the Product is Active or it belongs to the authorized Subject gateway - assert.strictEqual( - prodEnv.active === true || - prodEnv.product.namespace === context.authedItem.namespace, - true, - 'Product not elligible for requesting access' - ); + // SDX : Temporarily remove this check + // as it will require a bit of thought on where the "namespace" comes from + // this is more of an entitlement check + // assert.strictEqual( + // prodEnv.active === true || + // prodEnv.product.namespace === context.authedItem.namespace, + // true, + // 'Product not elligible for requesting access' + // ); // assert that the Consumer does not already exist const application = await lookupMyApplicationsById( From 5bfbe186c1dbbb5f8062fc3ffda72d531ad4037d Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 10:02:26 -0700 Subject: [PATCH 060/109] remove need for passing userid to create req --- src/controllers/v3/OrgAccessRequestsController.ts | 1 + src/controllers/v3/types-extra.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index 3845ee237..a462958b9 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -114,6 +114,7 @@ export class OrgAccessRequestsController extends Controller { const ctx = await this.keystone.createContextithUser(request, true); body.org = org; + body.userId = ctx.authedItem.userId; const result = await this.keystone.executeGraphQL({ context: ctx, diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 678084775..48e3938e8 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -31,7 +31,7 @@ export interface GatewayAdd { export interface OrgAccessRequestCreateInput { org?: string; orgMemberId: string; - userId: string; + userId?: string; consumerProductEnvAppId: string; providerProductEnvAppId: string; businessProcess: string; From 1a51d0cf49667fe2feedc1b4c5c75eece87be207 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 10:37:39 -0700 Subject: [PATCH 061/109] minor fix on create req --- src/controllers/v3/OrgAccessRequestsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index a462958b9..fe330b3c2 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -131,7 +131,7 @@ export class OrgAccessRequestsController extends Controller { throw new ValidateError(errors, 'Unable to create Access Request'); } return { - id: result.data.orgAccessRequest.id, + id: result.data.orgCreateAccessRequest.id, }; } From af60cb421706fd38f51d60d4708119f45a1c8a1f Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 14:22:43 -0700 Subject: [PATCH 062/109] add apispec update --- src/batch/data-rules.js | 6 +- src/controllers/v2/openapi.yaml | 12 +- src/controllers/v2/routes.ts | 8 +- src/controllers/v2/types.ts | 6 - .../v3/GatewayServicesController.ts | 1 + src/controllers/v3/OrgAPISpecController.ts | 65 +++++++++ .../v3/OrgAccessRequestsController.ts | 2 +- src/controllers/v3/openapi.yaml | 110 ++++++++++++-- src/controllers/v3/routes.ts | 85 ++++++++++- src/controllers/v3/types-extra.ts | 5 + src/controllers/v3/types.ts | 6 - .../integrated/keystonejs/accessRequest.ts | 31 +++- .../integrated/keystonejs/product-apispec.ts | 135 ++++++++++++++++++ .../integrated/workflow/namespace-activity.ts | 9 +- 14 files changed, 438 insertions(+), 43 deletions(-) create mode 100644 src/controllers/v3/OrgAPISpecController.ts create mode 100644 src/test/integrated/keystonejs/product-apispec.ts diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index a9fca2957..318f441bf 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -453,7 +453,7 @@ const metadata = { 'name', { key: 'parent.id', whereClause: 'product: { id: $parent_id }' }, ], - sync: ['name', 'active', 'approval', 'flow', 'additionalDetailsToRequest', 'spec'], + sync: ['name', 'active', 'approval', 'flow', 'additionalDetailsToRequest'], ownedBy: 'product', transformations: { services: { @@ -463,7 +463,9 @@ const metadata = { filterByNamespace: true, }, legal: { name: 'connectOne', list: 'allLegals', refKey: 'reference' }, - spec: { name: 'connectOne', list: 'allBlobs', refKey: 'name' }, + // exclude "spec" otherwise batch reading will automatically try to read it + // which we don't necessarily want + // spec: { name: 'connectOne', list: 'allBlobs', refKey: 'name' }, credentialIssuer: { name: 'connectOne', list: 'allCredentialIssuers', diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index ba1a8eff2..adf5428f9 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -580,6 +580,12 @@ components: type: string enabled: type: boolean + permDataPlane: + type: string + permDomains: + items: + type: string + type: array updatedAt: type: number format: double @@ -587,6 +593,8 @@ components: - name - orgUnit - enabled + - permDataPlane + - permDomains - updatedAt type: object additionalProperties: false @@ -594,8 +602,6 @@ components: type: string LegalRefID: type: string - BlobRefID: - type: string CredentialIssuerRefID: type: string Environment: @@ -632,8 +638,6 @@ components: type: array legal: $ref: '#/components/schemas/LegalRefID' - spec: - $ref: '#/components/schemas/BlobRefID' credentialIssuer: $ref: '#/components/schemas/CredentialIssuerRefID' type: object diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 674b8af49..f09885882 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -373,6 +373,8 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string","required":true}, "orgUnit": {"dataType":"string","required":true}, "enabled": {"dataType":"boolean","required":true}, + "permDataPlane": {"dataType":"string","required":true}, + "permDomains": {"dataType":"array","array":{"dataType":"string"},"required":true}, "updatedAt": {"dataType":"double","required":true}, }, "additionalProperties": false, @@ -388,11 +390,6 @@ const models: TsoaRoute.Models = { "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 - "BlobRefID": { - "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 "CredentialIssuerRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -409,7 +406,6 @@ const models: TsoaRoute.Models = { "additionalDetailsToRequest": {"dataType":"string"}, "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, "legal": {"ref":"LegalRefID"}, - "spec": {"ref":"BlobRefID"}, "credentialIssuer": {"ref":"CredentialIssuerRefID"}, }, "additionalProperties": false, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index 1ed2543bb..fac6c5660 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -319,7 +319,6 @@ export interface Environment { additionalDetailsToRequest?: string; services?: GatewayServiceRefID[]; legal?: LegalRefID; - spec?: BlobRefID; credentialIssuer?: CredentialIssuerRefID; } @@ -547,11 +546,6 @@ export interface DatasetResource { */ export type ApplicationRefID = string -/** - * @tsoaModel - */ -export type BlobRefID = string - /** * @tsoaModel */ diff --git a/src/controllers/v3/GatewayServicesController.ts b/src/controllers/v3/GatewayServicesController.ts index eb902cce6..7d8bf894e 100644 --- a/src/controllers/v3/GatewayServicesController.ts +++ b/src/controllers/v3/GatewayServicesController.ts @@ -38,6 +38,7 @@ export class GatewayController extends Controller { @OperationId('publish-gateway-config') @Security('jwt', ['Gateway.Config']) public async put( + @Path() gatewayId: string, @FormField() dryRun: boolean, @UploadedFile() configFile: Express.Multer.File ): Promise { diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts new file mode 100644 index 000000000..1aa9c9ce8 --- /dev/null +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -0,0 +1,65 @@ +import { + Controller, + Request, + OperationId, + Put, + Path, + Route, + Security, + Body, Tags +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { OrgAPISpecCreateInput } from './types-extra'; +import { Logger } from '../../logger'; +import { gql } from 'graphql-request'; +import UpdateAPISpec from '../../services/workflow/update-api-spec'; + +const logger = Logger('controllers.OrgAPISpec'); + +@injectable() +@Route('/organizations') +@Tags('Organizations') +export class OrgAPISpecController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Update API Specification for a Product Environment + * > `Required Scope:` Namespace.Assign + * + * @summary Manage Access Requests + * @param ns + * @param body + * @param request + */ + @Put('/{org}/api_specs') + @OperationId('organization-put-access-requests') + @Security('jwt', ['Namespace.Assign']) + public async put( + @Path() org: string, + @Body() body: OrgAPISpecCreateInput, + @Request() request: any + ): Promise<{id: string}> { + const ctx = await this.keystone.createContextithUser(request, true); + + return await UpdateAPISpec(ctx, body.specUrl, body.productEnvAppId); + } +} + +const createAccessRequest = gql` + mutation OrgAccessRequestCreate ($data: OrgAccessRequestCreateInput) { + orgCreateAccessRequest (data: $data) { + application { + appId + } + accessRequest { + id + } + } + } +`; + diff --git a/src/controllers/v3/OrgAccessRequestsController.ts b/src/controllers/v3/OrgAccessRequestsController.ts index fe330b3c2..756354afd 100644 --- a/src/controllers/v3/OrgAccessRequestsController.ts +++ b/src/controllers/v3/OrgAccessRequestsController.ts @@ -32,7 +32,7 @@ const logger = Logger('controllers.OrgAccessReq'); @injectable() @Route('/organizations') -@Tags('API Directory (Administration)') +@Tags('Organizations') export class OrgAccessRequestsController extends Controller { private keystone: KeystoneService; constructor(@inject('KeystoneService') private _keystone: KeystoneService) { diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 3ebb024d6..9b57355ad 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -478,6 +478,8 @@ components: additionalProperties: false OrgAccessRequestCreateInput: properties: + org: + type: string orgMemberId: type: string userId: @@ -496,7 +498,6 @@ components: type: array required: - orgMemberId - - userId - consumerProductEnvAppId - providerProductEnvAppId - businessProcess @@ -632,6 +633,12 @@ components: type: string enabled: type: boolean + permDataPlane: + type: string + permDomains: + items: + type: string + type: array updatedAt: type: number format: double @@ -639,6 +646,8 @@ components: - name - orgUnit - enabled + - permDataPlane + - permDomains - updatedAt type: object additionalProperties: false @@ -656,12 +665,21 @@ components: type: string type: object additionalProperties: false + OrgAPISpecCreateInput: + properties: + productEnvAppId: + type: string + specUrl: + type: string + required: + - productEnvAppId + - specUrl + type: object + additionalProperties: false DraftDatasetRefID: type: string LegalRefID: type: string - BlobRefID: - type: string CredentialIssuerRefID: type: string Environment: @@ -698,8 +716,6 @@ components: type: array legal: $ref: '#/components/schemas/LegalRefID' - spec: - $ref: '#/components/schemas/BlobRefID' credentialIssuer: $ref: '#/components/schemas/CredentialIssuerRefID' type: object @@ -1274,7 +1290,13 @@ paths: - jwt: - Gateway.Config - parameters: [] + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string requestBody: required: true content: @@ -1444,7 +1466,7 @@ paths: description: "Get Access Requests that are available by API for this organization\n> `Required Scope:` Namespace.Assign" summary: 'Get Organization Access Requests' tags: - - 'API Directory (Administration)' + - Organizations security: - jwt: @@ -1464,11 +1486,15 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OrgAccessRequest' + properties: + id: {type: string} + required: + - id + type: object description: "Manage Access Requests for APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Assign" summary: 'Manage Access Requests' tags: - - 'API Directory (Administration)' + - Organizations security: - jwt: @@ -1486,6 +1512,37 @@ paths: application/json: schema: $ref: '#/components/schemas/OrgAccessRequestCreateInput' + '/organizations/{org}/access_requests/{id}': + delete: + operationId: organization-delete-access-request + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: {} + type: object + description: "Delete Access Request\n> `Required Scope:` Namespace.Assign" + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: id + required: true + schema: + type: string /organizations: get: operationId: organization-list @@ -1808,6 +1865,41 @@ paths: default: 0 format: double type: number + '/organizations/{org}/api_specs': + put: + operationId: organization-put-access-requests + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + id: {type: string} + required: + - id + type: object + description: "Update API Specification for a Product Environment\n> `Required Scope:` Namespace.Assign" + summary: 'Manage Access Requests' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrgAPISpecCreateInput' '/organizations/{org}/products': get: operationId: organization-products diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index e7f58e7f0..5998d620a 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -25,6 +25,8 @@ import { OrgAccessRequestsController } from './OrgAccessRequestsController'; // 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 { OrganizationController } from './OrganizationController'; // 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 { OrgAPISpecController } from './OrgAPISpecController'; +// 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 { OrgProductController } from './OrgProductController'; // 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 { OrgRoleController } from './OrgRoleController'; @@ -272,8 +274,9 @@ const models: TsoaRoute.Models = { "OrgAccessRequestCreateInput": { "dataType": "refObject", "properties": { + "org": {"dataType":"string"}, "orgMemberId": {"dataType":"string","required":true}, - "userId": {"dataType":"string","required":true}, + "userId": {"dataType":"string"}, "consumerProductEnvAppId": {"dataType":"string","required":true}, "providerProductEnvAppId": {"dataType":"string","required":true}, "businessProcess": {"dataType":"string","required":true}, @@ -376,6 +379,8 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string","required":true}, "orgUnit": {"dataType":"string","required":true}, "enabled": {"dataType":"boolean","required":true}, + "permDataPlane": {"dataType":"string","required":true}, + "permDomains": {"dataType":"array","array":{"dataType":"string"},"required":true}, "updatedAt": {"dataType":"double","required":true}, }, "additionalProperties": false, @@ -393,17 +398,21 @@ 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 - "DraftDatasetRefID": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{}}, + "OrgAPISpecCreateInput": { + "dataType": "refObject", + "properties": { + "productEnvAppId": {"dataType":"string","required":true}, + "specUrl": {"dataType":"string","required":true}, + }, + "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 - "LegalRefID": { + "DraftDatasetRefID": { "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 - "BlobRefID": { + "LegalRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, }, @@ -424,7 +433,6 @@ const models: TsoaRoute.Models = { "additionalDetailsToRequest": {"dataType":"string"}, "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, "legal": {"ref":"LegalRefID"}, - "spec": {"ref":"BlobRefID"}, "credentialIssuer": {"ref":"CredentialIssuerRefID"}, }, "additionalProperties": false, @@ -1004,6 +1012,7 @@ export function RegisterRoutes(app: express.Router) { async function GatewayController_put(request: any, response: any, next: any) { const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, dryRun: {"in":"formData","name":"dryRun","required":true,"dataType":"string"}, configFile: {"in":"formData","name":"configFile","required":true,"dataType":"file"}, }; @@ -1209,6 +1218,37 @@ export function RegisterRoutes(app: express.Router) { } }); // 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.delete('/ds/api/v3/organizations/:org/access_requests/:id', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrgAccessRequestsController_deleteRequest(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + id: {"in":"path","name":"id","required":true,"dataType":"string"}, + 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(OrgAccessRequestsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.deleteRequest.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/v3/organizations/:org/access_requests', authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), @@ -1568,6 +1608,37 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/:org/api_specs', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrgAPISpecController_put(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"OrgAPISpecCreateInput"}, + 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(OrgAPISpecController); + 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/v3/organizations/:org/products', authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 48e3938e8..a9898bd39 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -28,6 +28,11 @@ export interface GatewayAdd { dataPlane?: string; } +export interface OrgAPISpecCreateInput { + productEnvAppId: string; + specUrl: string; +} + export interface OrgAccessRequestCreateInput { org?: string; orgMemberId: string; diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index 91f9f2ade..c7aa565bb 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -319,7 +319,6 @@ export interface Environment { additionalDetailsToRequest?: string; services?: GatewayServiceRefID[]; legal?: LegalRefID; - spec?: BlobRefID; credentialIssuer?: CredentialIssuerRefID; } @@ -547,11 +546,6 @@ export interface DatasetResource { */ export type ApplicationRefID = string -/** - * @tsoaModel - */ -export type BlobRefID = string - /** * @tsoaModel */ diff --git a/src/test/integrated/keystonejs/accessRequest.ts b/src/test/integrated/keystonejs/accessRequest.ts index 7b1886181..06a4a0789 100644 --- a/src/test/integrated/keystonejs/accessRequest.ts +++ b/src/test/integrated/keystonejs/accessRequest.ts @@ -50,9 +50,10 @@ import { getGwaProductEnvironment, getOrgNamespaces, } from '../../../services/workflow/get-namespaces'; -import { getRecords, replaceKey } from '../../../batch/feed-worker'; +import { deleteRecord, getRecords, replaceKey, syncRecordsThrowErrors } from '../../../batch/feed-worker'; import { OrgAccessRequestCreate } from '../../../services/workflow/org-access-request'; import { OrgAccessRequestCreateInput } from '../../../services/workflow/types'; +import { lookupServiceAccessesByNamespace } from '../../../services/keystone'; (async () => { const keystone = await InitKeystone(); @@ -78,6 +79,18 @@ import { OrgAccessRequestCreateInput } from '../../../services/workflow/types'; authentication: { item: identity }, }); + if (true) { + const result = await lookupServiceAccessesByNamespace(ctx, ns); + o(result); + + + + // const res = await deleteRecord(ctx, 'GatewayConsumer', 'b5f06ded-0c3d-4cb7-802a-ed3c03d5cbf8'); + // o(res); + // const revoke = await revokeAllConsumerAccess(ctx, ns, request.serviceAccess.id); + // o(revoke); + } + if (true) { const result = await ctx.executeGraphQL({ query: ` @@ -131,6 +144,22 @@ import { OrgAccessRequestCreateInput } from '../../../services/workflow/types'; }, }); o(result); + + /* +{ + "org": "ministry-of-puppies-and-kittens", + "orgMemberId": "MIN/PUKI", + "userId": "12", + "consumerProductEnvAppId": "E7FEB796", + "providerProductEnvAppId": "1400BE49", + "businessProcess": "Vet Services", + "accessPointDN": "CN=sdx.gov.bc.ca", + "optionalClientScopes": [ + "user/Test2" + ] +} + */ + } if (false) { diff --git a/src/test/integrated/keystonejs/product-apispec.ts b/src/test/integrated/keystonejs/product-apispec.ts new file mode 100644 index 000000000..ad2c5ccbd --- /dev/null +++ b/src/test/integrated/keystonejs/product-apispec.ts @@ -0,0 +1,135 @@ +/* +Wire up directly with Keycloak and use the Services +To run: +npm run ts-build +npm run ts-watch +node dist/test/integrated/keystonejs/product-apispec.js +*/ + +import InitKeystone from './init'; +import { + getRecords, + deleteRecord, + parseJsonString, + transformAllRefID, + removeEmpty, + removeKeys, + syncRecords, + parseBlobString, +} from '../../../batch/feed-worker'; +import { o } from '../util'; +import { lookupServiceAccessesByEnvironment } from '../../../services/keystone'; +import { + getActivity, + recordActivity, + recordActivityWithBlob, +} from '../../../services/keystone/activity'; +import { id } from 'date-fns/locale'; +import UpdateAPISpec from '../../../services/workflow/update-api-spec'; + +(async () => { + const keystone = await InitKeystone(); + console.log('K = ' + keystone); + + const ns = 'gw-31a33'; + const skipAccessControl = true; + + const userId = '12'; + + const identity = { + id: null, + username: 'sample_username', + name: 'SampleF UserL', + namespace: ns, + roles: JSON.stringify(['api-owner']), + scopes: [], + userId, + } as any; + + const ctx = keystone.createContext({ + skipAccessControl, + authentication: { item: identity }, + }); + + if (true) { + const spec= 'https://bcgov.github.io/sdx-openapi/%3CService%3E.v1.yaml'; + const result = await UpdateAPISpec(ctx, spec, 'E7FEB796'); + o(result); + } + + if (false) { + // upgrade + + + const variables = { + id: '20', + namespace: ns, + blobRef: 'B1678A2ADDD0-APISPEC-v1', + blobType: 'yaml', + blob: `gateway: gw-31a33 +patterns: + - name: pattern-1 + description: Pattern 1 + apis: + `, + }; + + const blobExists = await ctx.executeGraphQL({ + query: `query ($blobRef: String!) { + allBlobs (where: { ref: $blobRef}) { + id + } + }`, + variables, + }); + o(blobExists); + if (blobExists.data.allBlobs.length == 1) { + const result = await ctx.executeGraphQL({ + query: `mutation ($id: String!) { + deleteBlob (id: $id) { + id + } + }`, + variables: { id: blobExists.data.allBlobs[0].id }, + }); + o(result); + } + + const result = await ctx.executeGraphQL({ + query: `mutation ($id: String, $oldBlobId: String, $blob: String, $blobType: String, $blobRef: String) { + updateEnvironment (id: $id, data: { + spec: { + create: { + ref: $blobRef, + type: $blobType, + blob: $blob + } + } + }) { + id + spec { + id + } + } + }`, + variables, + }); + o(result); + + const getSpec = await ctx.executeGraphQL({ + query: `query ($blobRef: String!) { + allEnvironments (where: { spec: { ref: $blobRef}}) { + id + spec { + blob + } + } + }`, + variables, + }); + o(getSpec); + console.log(getSpec.data.allEnvironments[0].spec.blob); + } + + await keystone.disconnect(); +})(); diff --git a/src/test/integrated/workflow/namespace-activity.ts b/src/test/integrated/workflow/namespace-activity.ts index be3b24e65..7927ebe4b 100644 --- a/src/test/integrated/workflow/namespace-activity.ts +++ b/src/test/integrated/workflow/namespace-activity.ts @@ -9,7 +9,9 @@ node dist/test/integrated/workflow/namespace-activity.js import InitKeystone from '../keystonejs/init'; import { o } from '../util'; import { getFilteredNamespaceActivity } from '../../../services/workflow'; -import { ActivityQueryFilter } from '@/services/workflow/types'; +import { ActivityQueryFilter } from '../../../services/workflow/types'; +import { getAllNamespaces } from '../../../services/keycloak/namespace-details'; +import { getGwaProductEnvironment, getOrgNamespaces } from '../../../services/workflow/get-namespaces'; (async () => { const keystone = await InitKeystone(); @@ -39,6 +41,11 @@ import { ActivityQueryFilter } from '@/services/workflow/types'; const a = await getFilteredNamespaceActivity(ctx, ns, 20, 0, filter); o(a); + const prodEnv = await getGwaProductEnvironment(ctx, false); + + const nsList = await getOrgNamespaces("ministry-of-citizens-services", prodEnv); + o(nsList); + // 2022-09-13T16:47:09.367Z await keystone.disconnect(); })(); From 5d68b2d29fd8e34925869ddb1bba5d303a811750 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 14:28:16 -0700 Subject: [PATCH 063/109] add get api_spec --- src/controllers/v3/OrgAPISpecController.ts | 48 +++++++---- src/services/workflow/update-api-spec.ts | 98 ++++++++++++++++++++++ 2 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 src/services/workflow/update-api-spec.ts diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index 1aa9c9ce8..c806b0ce2 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -6,7 +6,8 @@ import { Path, Route, Security, - Body, Tags + Body, Tags, + Get } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; @@ -46,20 +47,39 @@ export class OrgAPISpecController extends Controller { ): Promise<{id: string}> { const ctx = await this.keystone.createContextithUser(request, true); - return await UpdateAPISpec(ctx, body.specUrl, body.productEnvAppId); - } -} + const result = await UpdateAPISpec(ctx, body.specUrl, body.productEnvAppId); + logger.debug('OrgAPISpecController: %j', result); + return {id: result.id} + } -const createAccessRequest = gql` - mutation OrgAccessRequestCreate ($data: OrgAccessRequestCreateInput) { - orgCreateAccessRequest (data: $data) { - application { - appId - } - accessRequest { - id - } + @Get('/{org}/api_specs') + @OperationId('organization-get-api-specs') + @Security('jwt', ['Namespace.Assign']) + public async get( + @Path() org: string, + @Request() request: any + ): Promise { + const ctx = await this.keystone.createContextithUser(request, true); + const query = gql` + query getProductEnvironments($org: String) { + allEnvironments(where: { product: { organization: { name: $org } } }) { + id + spec { + id + blob + } + } + `; + + const specs = await ctx.executeGraphQL({ + query, + variables: { org }, + }); + if (specs.errors) { + logger.error('Error fetching Specs %j', specs.errors); + throw new Error('Error fetching Specs'); } + return specs.data.allEnvironments.filter((env:any) => env.spec).map((env: any) => env.spec); } -`; +} diff --git a/src/services/workflow/update-api-spec.ts b/src/services/workflow/update-api-spec.ts new file mode 100644 index 000000000..7b83c2c00 --- /dev/null +++ b/src/services/workflow/update-api-spec.ts @@ -0,0 +1,98 @@ +import { FieldErrors, ValidateError } from 'tsoa'; +import { Logger } from '../../logger'; +import YAML from 'js-yaml'; +import {strict as assert} from 'assert'; +import { id } from 'date-fns/locale'; + +const logger = Logger('wf.UpdAPISpec'); + +async function UpdateAPISpec(ctx: any, specUrl: string, productEnvAppId: string) { + + // Fetch the specUrl and parse YAML + const response = await fetch(specUrl); + if (!response.ok) { + throw new ValidateError({}, `Failed to fetch spec from URL: ${specUrl}`); + } + const yamlText = await response.text(); + let parsedYaml: any; + try { + parsedYaml = YAML.load(yamlText); + } catch (err) { + logger.error('YAML parsing error: %j', err); + throw new ValidateError({}, 'Invalid YAML format'); + } + + const blobRef = `${productEnvAppId}-APISPEC-${parsedYaml.info.version}`; + + const blobExists = await ctx.executeGraphQL({ + query: `query ($blobRef: String!) { + allEnvironments (where: { appId: "${productEnvAppId}"}) { + id + } + + allBlobs (where: { ref: $blobRef}) { + id + } + }`, + variables: { appId: productEnvAppId, blobRef }, + }); + + assert(!blobExists.errors, 'Unable to delete existing Blob'); + + const variables = { + id: blobExists.data.allEnvironments[0].id, + blobRef, + blobType: 'yaml', + blob: YAML.dump(parsedYaml), + }; + + if (blobExists.data.allBlobs.length == 1) { + const result = await ctx.executeGraphQL({ + query: `mutation ($id: String!) { + deleteBlob (id: $id) { + id + } + }`, + variables: { id: blobExists.data.allBlobs[0].id }, + }); + assert(!result.errors, 'Unable to delete existing Blob'); + } + + const result = await ctx.executeGraphQL({ + query: `mutation ($id: String, $oldBlobId: String, $blob: String, $blobType: String, $blobRef: String) { + updateEnvironment (id: $id, data: { + spec: { + create: { + ref: $blobRef, + type: $blobType, + blob: $blob + } + } + }) { + id + spec { + id + } + } + }`, + variables, + }); + + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + logger.error('%j', result); + throw new ValidateError(errors, 'Unable to update API Specification'); + } + return { + id: result.data.updateEnvironment.id, + spec: { + id: result.data.updateEnvironment.spec.id, + } + }; + +} + +export default UpdateAPISpec; \ No newline at end of file From 3130e51a4d2614b2d1c0ee904c21c4fc21a0b50c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 14:31:25 -0700 Subject: [PATCH 064/109] tweak api_spec --- src/controllers/v3/OrgAPISpecController.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index c806b0ce2..75397d89a 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -58,12 +58,13 @@ export class OrgAPISpecController extends Controller { public async get( @Path() org: string, @Request() request: any - ): Promise { + ): Promise<{prodEnvId: string, spec: string}> { const ctx = await this.keystone.createContextithUser(request, true); const query = gql` query getProductEnvironments($org: String) { allEnvironments(where: { product: { organization: { name: $org } } }) { id + appId spec { id blob @@ -79,7 +80,7 @@ export class OrgAPISpecController extends Controller { logger.error('Error fetching Specs %j', specs.errors); throw new Error('Error fetching Specs'); } - return specs.data.allEnvironments.filter((env:any) => env.spec).map((env: any) => env.spec); + return specs.data.allEnvironments.filter((env:any) => env.spec).map((env: any) => ( + {prodEnvId: env.appId, spec: env.spec})); } - } From 03ad2f3813433372302747a112a5d84bfc3fd269 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 14:31:58 -0700 Subject: [PATCH 065/109] tweak api_spec --- src/controllers/v3/openapi.yaml | 28 ++++++++++++++++++++++++++++ src/controllers/v3/routes.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 9b57355ad..948edb5c3 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -1900,6 +1900,34 @@ paths: application/json: schema: $ref: '#/components/schemas/OrgAPISpecCreateInput' + get: + operationId: organization-get-api-specs + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + spec: {type: string} + prodEnvId: {type: string} + required: + - spec + - prodEnvId + type: object + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string '/organizations/{org}/products': get: operationId: organization-products diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 5998d620a..219f17db4 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -1639,6 +1639,36 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/:org/api_specs', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrgAPISpecController_get(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + 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(OrgAPISpecController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.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/v3/organizations/:org/products', authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), From dca955a04fb8cf35f27820723171463c45692672 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Sep 2025 15:13:57 -0700 Subject: [PATCH 066/109] upd get apispecs --- src/controllers/v3/OrgAPISpecController.ts | 40 ++---- src/services/workflow/api-specs.ts | 127 ++++++++++++++++++ src/services/workflow/update-api-spec.ts | 98 -------------- .../integrated/keystonejs/product-apispec.ts | 6 +- 4 files changed, 143 insertions(+), 128 deletions(-) create mode 100644 src/services/workflow/api-specs.ts delete mode 100644 src/services/workflow/update-api-spec.ts diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index 75397d89a..15ab816d1 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -6,15 +6,16 @@ import { Path, Route, Security, - Body, Tags, - Get + Body, + Tags, + Get, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; import { OrgAPISpecCreateInput } from './types-extra'; import { Logger } from '../../logger'; import { gql } from 'graphql-request'; -import UpdateAPISpec from '../../services/workflow/update-api-spec'; +import { UpdateAPISpec, GetAPISpecsByOrg } from '../../services/workflow/api-specs'; const logger = Logger('controllers.OrgAPISpec'); @@ -44,12 +45,12 @@ export class OrgAPISpecController extends Controller { @Path() org: string, @Body() body: OrgAPISpecCreateInput, @Request() request: any - ): Promise<{id: string}> { + ): Promise<{ id: string }> { const ctx = await this.keystone.createContextithUser(request, true); const result = await UpdateAPISpec(ctx, body.specUrl, body.productEnvAppId); logger.debug('OrgAPISpecController: %j', result); - return {id: result.id} + return { id: result.id }; } @Get('/{org}/api_specs') @@ -58,29 +59,10 @@ export class OrgAPISpecController extends Controller { public async get( @Path() org: string, @Request() request: any - ): Promise<{prodEnvId: string, spec: string}> { - const ctx = await this.keystone.createContextithUser(request, true); - const query = gql` - query getProductEnvironments($org: String) { - allEnvironments(where: { product: { organization: { name: $org } } }) { - id - appId - spec { - id - blob - } - } - `; - - const specs = await ctx.executeGraphQL({ - query, - variables: { org }, - }); - if (specs.errors) { - logger.error('Error fetching Specs %j', specs.errors); - throw new Error('Error fetching Specs'); - } - return specs.data.allEnvironments.filter((env:any) => env.spec).map((env: any) => ( - {prodEnvId: env.appId, spec: env.spec})); + ): Promise<{ prodEnvId: string; spec: string }> { + const ctx = await this.keystone.createContextithUser(request, true); + const result = await GetAPISpecsByOrg(ctx, org); + logger.debug('OrgAPISpecController: %j', result); + return result; } } diff --git a/src/services/workflow/api-specs.ts b/src/services/workflow/api-specs.ts new file mode 100644 index 000000000..d409ce965 --- /dev/null +++ b/src/services/workflow/api-specs.ts @@ -0,0 +1,127 @@ +import { FieldErrors, ValidateError } from 'tsoa'; +import { Logger } from '../../logger'; +import YAML from 'js-yaml'; +import { strict as assert } from 'assert'; +import { id } from 'date-fns/locale'; +import { gql } from 'graphql-request'; + +const logger = Logger('wf.UpdAPISpec'); + +async function UpdateAPISpec( + ctx: any, + specUrl: string, + productEnvAppId: string +) { + // Fetch the specUrl and parse YAML + const response = await fetch(specUrl); + if (!response.ok) { + throw new ValidateError({}, `Failed to fetch spec from URL: ${specUrl}`); + } + const yamlText = await response.text(); + let parsedYaml: any; + try { + parsedYaml = YAML.load(yamlText); + } catch (err) { + logger.error('YAML parsing error: %j', err); + throw new ValidateError({}, 'Invalid YAML format'); + } + + const blobRef = `${productEnvAppId}-APISPEC-${parsedYaml.info.version}`; + + const blobExists = await ctx.executeGraphQL({ + query: `query ($blobRef: String!) { + allEnvironments (where: { appId: "${productEnvAppId}"}) { + id + } + + allBlobs (where: { ref: $blobRef}) { + id + } + }`, + variables: { appId: productEnvAppId, blobRef }, + }); + + assert(!blobExists.errors, 'Unable to delete existing Blob'); + + const variables = { + id: blobExists.data.allEnvironments[0].id, + blobRef, + blobType: 'yaml', + blob: YAML.dump(parsedYaml), + }; + + if (blobExists.data.allBlobs.length == 1) { + const result = await ctx.executeGraphQL({ + query: `mutation ($id: String!) { + deleteBlob (id: $id) { + id + } + }`, + variables: { id: blobExists.data.allBlobs[0].id }, + }); + assert(!result.errors, 'Unable to delete existing Blob'); + } + + const result = await ctx.executeGraphQL({ + query: `mutation ($id: String, $oldBlobId: String, $blob: String, $blobType: String, $blobRef: String) { + updateEnvironment (id: $id, data: { + spec: { + create: { + ref: $blobRef, + type: $blobType, + blob: $blob + } + } + }) { + id + spec { + id + } + } + }`, + variables, + }); + + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + logger.error('%j', result); + throw new ValidateError(errors, 'Unable to update API Specification'); + } + return { + id: result.data.updateEnvironment.id, + spec: { + id: result.data.updateEnvironment.spec.id, + }, + }; +} + +async function GetAPISpecsByOrg(ctx: any, org: string) { + const query = gql` + query getProductEnvironments($org: String) { + allEnvironments(where: { product: { organization: { name: $org } } }) { + id + appId + spec { + id + blob + } + } + } + `; + + const specs = await ctx.executeGraphQL({ + query, + variables: { org }, + }); + if (specs.errors) { + logger.error('Error fetching Specs %j', specs.errors); + throw new Error('Error fetching Specs'); + } + return specs.data.allEnvironments + .filter((env: any) => env.spec) + .map((env: any) => ({ prodEnvId: env.appId, spec: env.spec })); +} +export { UpdateAPISpec, GetAPISpecsByOrg }; diff --git a/src/services/workflow/update-api-spec.ts b/src/services/workflow/update-api-spec.ts deleted file mode 100644 index 7b83c2c00..000000000 --- a/src/services/workflow/update-api-spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { FieldErrors, ValidateError } from 'tsoa'; -import { Logger } from '../../logger'; -import YAML from 'js-yaml'; -import {strict as assert} from 'assert'; -import { id } from 'date-fns/locale'; - -const logger = Logger('wf.UpdAPISpec'); - -async function UpdateAPISpec(ctx: any, specUrl: string, productEnvAppId: string) { - - // Fetch the specUrl and parse YAML - const response = await fetch(specUrl); - if (!response.ok) { - throw new ValidateError({}, `Failed to fetch spec from URL: ${specUrl}`); - } - const yamlText = await response.text(); - let parsedYaml: any; - try { - parsedYaml = YAML.load(yamlText); - } catch (err) { - logger.error('YAML parsing error: %j', err); - throw new ValidateError({}, 'Invalid YAML format'); - } - - const blobRef = `${productEnvAppId}-APISPEC-${parsedYaml.info.version}`; - - const blobExists = await ctx.executeGraphQL({ - query: `query ($blobRef: String!) { - allEnvironments (where: { appId: "${productEnvAppId}"}) { - id - } - - allBlobs (where: { ref: $blobRef}) { - id - } - }`, - variables: { appId: productEnvAppId, blobRef }, - }); - - assert(!blobExists.errors, 'Unable to delete existing Blob'); - - const variables = { - id: blobExists.data.allEnvironments[0].id, - blobRef, - blobType: 'yaml', - blob: YAML.dump(parsedYaml), - }; - - if (blobExists.data.allBlobs.length == 1) { - const result = await ctx.executeGraphQL({ - query: `mutation ($id: String!) { - deleteBlob (id: $id) { - id - } - }`, - variables: { id: blobExists.data.allBlobs[0].id }, - }); - assert(!result.errors, 'Unable to delete existing Blob'); - } - - const result = await ctx.executeGraphQL({ - query: `mutation ($id: String, $oldBlobId: String, $blob: String, $blobType: String, $blobRef: String) { - updateEnvironment (id: $id, data: { - spec: { - create: { - ref: $blobRef, - type: $blobType, - blob: $blob - } - } - }) { - id - spec { - id - } - } - }`, - variables, - }); - - if (result.errors) { - const errors: FieldErrors = {}; - result.errors.forEach((err: any, ind: number) => { - errors[`d${ind}`] = { message: err.message }; - }); - logger.error('%j', result); - throw new ValidateError(errors, 'Unable to update API Specification'); - } - return { - id: result.data.updateEnvironment.id, - spec: { - id: result.data.updateEnvironment.spec.id, - } - }; - -} - -export default UpdateAPISpec; \ No newline at end of file diff --git a/src/test/integrated/keystonejs/product-apispec.ts b/src/test/integrated/keystonejs/product-apispec.ts index ad2c5ccbd..856012a45 100644 --- a/src/test/integrated/keystonejs/product-apispec.ts +++ b/src/test/integrated/keystonejs/product-apispec.ts @@ -25,7 +25,7 @@ import { recordActivityWithBlob, } from '../../../services/keystone/activity'; import { id } from 'date-fns/locale'; -import UpdateAPISpec from '../../../services/workflow/update-api-spec'; +import {UpdateAPISpec, GetAPISpecsByOrg} from '../../../services/workflow/api-specs'; (async () => { const keystone = await InitKeystone(); @@ -52,6 +52,10 @@ import UpdateAPISpec from '../../../services/workflow/update-api-spec'; }); if (true) { + const result = await GetAPISpecsByOrg(ctx, 'ministry-of-puppies-and-kittens'); + o(result); + } + if (false) { const spec= 'https://bcgov.github.io/sdx-openapi/%3CService%3E.v1.yaml'; const result = await UpdateAPISpec(ctx, spec, 'E7FEB796'); o(result); From 97bee659507b3d4743741c7d26290f7607dfc73a Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 8 Sep 2025 14:34:48 -0700 Subject: [PATCH 067/109] fix org unit issue --- src/batch/data-rules.js | 1 + src/batch/feed-worker.ts | 3 ++- src/test/integrated/keystonejs/batch.ts | 36 +++++++++++++++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 318f441bf..4a8006ebb 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -19,6 +19,7 @@ const metadata = { name: 'connectExclusiveListCreate', list: 'OrganizationUnit', syncFirst: true, + refKey: 'extForeignKey', }, }, }, diff --git a/src/batch/feed-worker.ts b/src/batch/feed-worker.ts index 57086fcab..311031c96 100644 --- a/src/batch/feed-worker.ts +++ b/src/batch/feed-worker.ts @@ -221,7 +221,7 @@ function buildQueryResponse(md: any, children: string[] = undefined): string[] { .slice(); response.push(md.refKey); - logger.debug('[buildQueryResponse] DRAFT (%s) %j', md.query, response); + logger.debug('[buildQueryResponse] DRAFT (%s) (%s) %j', children, md.query, response); if (children) { relationshipFields.forEach((field: string) => { // populate the fields as well @@ -511,6 +511,7 @@ export const syncRecords = async function ( const transformInfo = md.transformations[transformKey]; if (transformInfo.syncFirst) { // handle these children independently first - return a list of IDs + const allIds = await syncListOfRecords( context, transformInfo, diff --git a/src/test/integrated/keystonejs/batch.ts b/src/test/integrated/keystonejs/batch.ts index ccd982df4..0bc3518e8 100644 --- a/src/test/integrated/keystonejs/batch.ts +++ b/src/test/integrated/keystonejs/batch.ts @@ -13,6 +13,7 @@ import { transformAllRefID, removeEmpty, removeKeys, + syncRecordsThrowErrors, } from '../../../batch/feed-worker'; import { o } from '../util'; import { BatchService } from '../../../services/keystone/batch-service'; @@ -22,7 +23,7 @@ import { BatchService } from '../../../services/keystone/batch-service'; console.log('K = ' + keystone); const ns = 'platform'; - const skipAccessControl = false; + const skipAccessControl = true; const identity = { id: null, @@ -97,10 +98,35 @@ import { BatchService } from '../../../services/keystone/batch-service'; const res = await bapi.lookup( 'allOrganizations', - 'orgUnits.name', - 'heritage', - [] + 'name', + 'ministry-of-citizens-services', + ['extForeignKey'] ); - o(res); + const id = res.extForeignKey; + const out = await syncRecordsThrowErrors( + ctx, + 'Organization', + id, + { + description: 'Updated desc 2', + extForeignKey: id, + orgUnits: [ + { + name: 'new-unit', + title: 'New Unity', + extForeignKey: '00001-new-unit', + // extSource: 'custom', + // extRecordHash: '1234', + // description: 'Newly created unit', + // tags: ['tag1', 'tag2'], + } + ] + } + , + true + ) + + + o(out); await keystone.disconnect(); })(); From a29af94eff278ac073d85261b45017b09de52e9e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 8 Sep 2025 16:00:53 -0700 Subject: [PATCH 068/109] exclude decommissioned ns --- src/services/org-groups/namespace.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/org-groups/namespace.ts b/src/services/org-groups/namespace.ts index 1fb0b087d..eb6caae71 100644 --- a/src/services/org-groups/namespace.ts +++ b/src/services/org-groups/namespace.ts @@ -156,7 +156,8 @@ export class NamespaceService { const matches = namespaceGroups .filter( (group) => - 'org' in group.attributes && group.attributes['org'][0] === org + 'org' in group.attributes && group.attributes['org'][0] === org && + !('decommissioned' in group.attributes) ) .map((group) => ({ name: group.name, From 983c65655dd20734d62d2dbe96c2a73a2bbb1a90 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 9 Sep 2025 19:47:29 -0700 Subject: [PATCH 069/109] enable consent --- src/services/keycloak/client-registration-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index 714dcdcb6..67a444851 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -106,6 +106,7 @@ export class KeycloakClientRegistrationService { enabled, name, clientId, + consentRequired: true, attributes: { 'x509.subjectdn': subjectDn } From bc396814a385ff70752469b1e23fd75a62fb98ad Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 9 Sep 2025 22:50:31 -0700 Subject: [PATCH 070/109] temporarily remove perms to get openapi spec --- src/controllers/v3/OrgAPISpecController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index 15ab816d1..2c95a00ea 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -55,7 +55,7 @@ export class OrgAPISpecController extends Controller { @Get('/{org}/api_specs') @OperationId('organization-get-api-specs') - @Security('jwt', ['Namespace.Assign']) + // @Security('jwt', ['Namespace.Assign']) public async get( @Path() org: string, @Request() request: any From 5dba438ab4a3ea879c72aef742a396030ad10dfb Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 9 Sep 2025 23:06:08 -0700 Subject: [PATCH 071/109] temporarily remove perms to get openapi spec --- src/controllers/v3/OrgAPISpecController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index 2c95a00ea..8335c1a92 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -55,7 +55,8 @@ export class OrgAPISpecController extends Controller { @Get('/{org}/api_specs') @OperationId('organization-get-api-specs') - // @Security('jwt', ['Namespace.Assign']) + @Security('jwt', []) + //@Security('jwt', ['Namespace.Assign']) public async get( @Path() org: string, @Request() request: any From b47048e17748b88b9be2d3c41f1f22aa87f57a6c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 9 Sep 2025 23:35:17 -0700 Subject: [PATCH 072/109] still working on org api spec --- src/controllers/v3/OrgAPISpecController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index 8335c1a92..c50948057 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -55,13 +55,12 @@ export class OrgAPISpecController extends Controller { @Get('/{org}/api_specs') @OperationId('organization-get-api-specs') - @Security('jwt', []) //@Security('jwt', ['Namespace.Assign']) public async get( @Path() org: string, @Request() request: any ): Promise<{ prodEnvId: string; spec: string }> { - const ctx = await this.keystone.createContextithUser(request, true); + const ctx = await this.keystone.createContext(request, true); const result = await GetAPISpecsByOrg(ctx, org); logger.debug('OrgAPISpecController: %j', result); return result; From 571cf72b4cafb774dc3d2abdb4309cabab327d96 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 9 Sep 2025 23:36:47 -0700 Subject: [PATCH 073/109] still working on org api spec --- src/controllers/v3/OrgAPISpecController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/OrgAPISpecController.ts b/src/controllers/v3/OrgAPISpecController.ts index c50948057..9c0718d82 100644 --- a/src/controllers/v3/OrgAPISpecController.ts +++ b/src/controllers/v3/OrgAPISpecController.ts @@ -60,7 +60,8 @@ export class OrgAPISpecController extends Controller { @Path() org: string, @Request() request: any ): Promise<{ prodEnvId: string; spec: string }> { - const ctx = await this.keystone.createContext(request, true); + const ctx = this.keystone.sudo(); + //const ctx = await this.keystone.createContext(request, true); const result = await GetAPISpecsByOrg(ctx, org); logger.debug('OrgAPISpecController: %j', result); return result; From 7d10656c17ecb440bd4d2a9dcdfd1d31db54958b Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 16:29:00 -0700 Subject: [PATCH 074/109] udp product catalog api --- src/controllers/v3/OrgProductController.ts | 91 ++++++++++++++++++++-- src/controllers/v3/openapi.yaml | 85 +++++++++++++++++++- src/controllers/v3/routes.ts | 50 +++++++++++- src/controllers/v3/types-extra.ts | 23 ++++++ 4 files changed, 237 insertions(+), 12 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 2221e4861..d87953daa 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -27,6 +27,10 @@ import { import { BatchResult } from '../../batch/types'; import { Dataset, DraftDataset } from './types'; import { Product } from './types'; +import { ProductCatalog, ProductCatalogOperation } from './types-extra'; +import { gql } from 'graphql-request'; +import { Environment } from '../../services/keystone/types'; +import YAML from 'yaml'; @injectable() @Route('/organizations') @@ -38,6 +42,66 @@ export class OrgProductController extends Controller { this.keystone = _keystone; } + /** + * Get Products that are available by API across all Organizations + * + * @summary Get Organization Datasets + */ + @Get('/catalog') + @OperationId('organization-products-catalog') + public async getProductCatalog( + @Request() request: any + ): Promise { + const result = await this.keystone.executeGraphQL({ + context: this.keystone.sudo(), + query: list, + }); + const envs = result.data.allEnvironments.filter( + (e: Environment) => e.product.organization != null + ); + + return envs.map((env: any) => { + const spec = YAML.parse(env.spec?.blob || '{}'); + + const operations = spec?.paths && Object.keys(spec.paths).map((path) => { + return Object.keys(spec.paths[path]).map((method) => { + const op = spec.paths[path][method]; + return { + operationId: op.operationId, + summary: op.summary || '', + scopes: (op.security && op.security[0] && op.security[0]['bearer_auth']) ? op.security[0]['bearer_auth'] : [], + }; + }); + }); + + const flattenedOperations = []; + if (operations) { + for (const opList of operations) { + for (const op of opList) { + flattenedOperations.push(op); + } + } + } + + return { + appId: env.appId, + name: env.name, + spec: { + title: spec.info?.title || '', + version: spec.info?.version || '', + description: spec.info?.description || '', + operations: flattenedOperations, + }, + product: { + name: env.product.name, + organization: { + name: env.product.organization.name, + }, + }, + }; + }); + } + /** * Get metadata about Datasets that are available by API for this organization * > `Required Scope:` Dataset.Manage @@ -70,14 +134,9 @@ export class OrgProductController extends Controller { return records .map((o) => removeEmpty(o)) .map((o) => transformAllRefID(o, ['organization', 'organizationUnit'])) - .map((o) => - removeKeys(o, [ - 'id' - ]) - ); + .map((o) => removeKeys(o, ['id'])); } - /** * Manage Products for APIs that will appear on the API Directory * > `Required Scope:` Namespace.Manage @@ -106,5 +165,23 @@ export class OrgProductController extends Controller { body['appId'], replaceKey(body, 'gatewayId', 'namespace') ); - } + } } + +const list = gql` + query OrgProductCatalog { + allEnvironments { + appId + name + spec { + blob + } + product { + name + organization { + name + } + } + } + } +`; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 948edb5c3..588014eef 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -676,6 +676,68 @@ components: - specUrl type: object additionalProperties: false + ProductCatalogOperation: + properties: + operationId: + type: string + summary: + type: string + scopes: + items: + type: string + type: array + required: + - operationId + - summary + - scopes + type: object + additionalProperties: false + ProductCatalog: + properties: + appId: + type: string + name: + type: string + spec: + properties: + operations: + items: + $ref: '#/components/schemas/ProductCatalogOperation' + type: array + description: + type: string + version: + type: string + title: + type: string + required: + - operations + - description + - version + - title + type: object + product: + properties: + organization: + properties: + name: + type: string + required: + - name + type: object + name: + type: string + required: + - organization + - name + type: object + required: + - appId + - name + - spec + - product + type: object + additionalProperties: false DraftDatasetRefID: type: string LegalRefID: @@ -1917,10 +1979,7 @@ paths: type: object tags: - Organizations - security: - - - jwt: - - Namespace.Assign + security: [] parameters: - in: path @@ -1928,6 +1987,24 @@ paths: required: true schema: type: string + /organizations/catalog: + get: + operationId: organization-products-catalog + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ProductCatalog' + type: array + description: 'Get Products that are available by API across all Organizations' + summary: 'Get Organization Datasets' + tags: + - 'API Directory (Administration)' + security: [] + parameters: [] '/organizations/{org}/products': get: operationId: organization-products diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 219f17db4..5e786d620 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -407,6 +407,27 @@ 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 + "ProductCatalogOperation": { + "dataType": "refObject", + "properties": { + "operationId": {"dataType":"string","required":true}, + "summary": {"dataType":"string","required":true}, + "scopes": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "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 + "ProductCatalog": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "spec": {"dataType":"nestedObjectLiteral","nestedProperties":{"operations":{"dataType":"array","array":{"dataType":"refObject","ref":"ProductCatalogOperation"},"required":true},"description":{"dataType":"string","required":true},"version":{"dataType":"string","required":true},"title":{"dataType":"string","required":true}},"required":true}, + "product": {"dataType":"nestedObjectLiteral","nestedProperties":{"organization":{"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}},"required":true},"name":{"dataType":"string","required":true}},"required":true}, + }, + "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 "DraftDatasetRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -1640,7 +1661,6 @@ export function RegisterRoutes(app: express.Router) { }); // 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/v3/organizations/:org/api_specs', - authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), async function OrgAPISpecController_get(request: any, response: any, next: any) { const args = { @@ -1669,6 +1689,34 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/catalog', + + async function OrgProductController_getProductCatalog(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(OrgProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getProductCatalog.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/v3/organizations/:org/products', authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index a9898bd39..99f3aa409 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -77,4 +77,27 @@ export interface OrgAccessRequest { }; }; createdAt: Scalars['DateTime']; +} + +export interface ProductCatalogOperation { + operationId: string; + summary: string; + scopes: string[]; +} + +export interface ProductCatalog { + appId: string; + name: string; + spec: { + title: string; + version: string; + description: string; + operations: ProductCatalogOperation[]; + } + product: { + name: string; + organization: { + name: string; + } + } } \ No newline at end of file From 4817161379d153efcb3fb71a5dd6cf3d256a084e Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 16:57:03 -0700 Subject: [PATCH 075/109] upd catalog list --- src/controllers/v3/OrgProductController.ts | 26 +++++++++++++++++++++- src/controllers/v3/openapi.yaml | 26 ++++++++++++++++++++++ src/controllers/v3/routes.ts | 1 + src/controllers/v3/types-extra.ts | 8 +++++++ src/services/org-groups/namespace.ts | 4 ++-- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index d87953daa..cf9a0f085 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -31,6 +31,9 @@ import { ProductCatalog, ProductCatalogOperation } from './types-extra'; import { gql } from 'graphql-request'; import { Environment } from '../../services/keystone/types'; import YAML from 'yaml'; +import { getGwaProductEnvironment } from '../../services/workflow'; +import { NamespaceService } from '../../services/org-groups'; +import { OrgNamespace } from '../../services/org-groups/types'; @injectable() @Route('/organizations') @@ -60,9 +63,10 @@ export class OrgProductController extends Controller { (e: Environment) => e.product.organization != null ); - return envs.map((env: any) => { + const output = envs.map((env: any) => { const spec = YAML.parse(env.spec?.blob || '{}'); + const operations = spec?.paths && Object.keys(spec.paths).map((path) => { return Object.keys(spec.paths[path]).map((method) => { const op = spec.paths[path][method]; @@ -100,6 +104,15 @@ export class OrgProductController extends Controller { }, }; }); + + const promises = output.filter((env:any) => env.product.namespace).map(async (env: any) => { + const nsAttributes = await getNamespaceAttributes( + env.product.namespace + ); + env.ns = nsAttributes; + }); + await Promise.all(promises); + return output; } /** @@ -178,6 +191,7 @@ const list = gql` } product { name + namespace organization { name } @@ -185,3 +199,13 @@ const list = gql` } } `; + + +async function getNamespaceAttributes(ns: string) : Promise { + const prodEnv = await getGwaProductEnvironment(this.keystone.sudo(), false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + return await svc.getNamespaceOrganizationDetails(ns); +} diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 588014eef..a64c0966d 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -731,11 +731,37 @@ components: - organization - name type: object + ns: + properties: + updatedAt: + type: number + format: double + enabled: + type: boolean + permDomains: + items: + type: string + type: array + permDataPlane: + type: string + orgUnit: + type: string + name: + type: string + required: + - updatedAt + - enabled + - permDomains + - permDataPlane + - orgUnit + - name + type: object required: - appId - name - spec - product + - ns type: object additionalProperties: false DraftDatasetRefID: diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 5e786d620..2904bf890 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -424,6 +424,7 @@ const models: TsoaRoute.Models = { "name": {"dataType":"string","required":true}, "spec": {"dataType":"nestedObjectLiteral","nestedProperties":{"operations":{"dataType":"array","array":{"dataType":"refObject","ref":"ProductCatalogOperation"},"required":true},"description":{"dataType":"string","required":true},"version":{"dataType":"string","required":true},"title":{"dataType":"string","required":true}},"required":true}, "product": {"dataType":"nestedObjectLiteral","nestedProperties":{"organization":{"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}},"required":true},"name":{"dataType":"string","required":true}},"required":true}, + "ns": {"dataType":"nestedObjectLiteral","nestedProperties":{"updatedAt":{"dataType":"double","required":true},"enabled":{"dataType":"boolean","required":true},"permDomains":{"dataType":"array","array":{"dataType":"string"},"required":true},"permDataPlane":{"dataType":"string","required":true},"orgUnit":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, }, "additionalProperties": false, }, diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 99f3aa409..cc67a75eb 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -100,4 +100,12 @@ export interface ProductCatalog { name: string; } } + ns: { + name: string; + orgUnit: string; + permDataPlane: string; + permDomains: string[]; + enabled: boolean; + updatedAt: number; + } } \ No newline at end of file diff --git a/src/services/org-groups/namespace.ts b/src/services/org-groups/namespace.ts index eb6caae71..d6519b2f0 100644 --- a/src/services/org-groups/namespace.ts +++ b/src/services/org-groups/namespace.ts @@ -184,10 +184,10 @@ export class NamespaceService { async getNamespaceOrganizationDetails(ns: string): Promise { const nsGroup = await this.groupService.findByName('ns', ns, false); - if ('org' in nsGroup.attributes && 'org-unit' in nsGroup.attributes) { + if ('org' in nsGroup.attributes) { return { name: nsGroup.attributes['org'].pop(), - orgUnit: nsGroup.attributes['org-unit'].pop(), + orgUnit: 'org-unit' in nsGroup.attributes ? nsGroup.attributes['org-unit'].pop() : '', permDataPlane: 'perm-data-plane' in nsGroup.attributes ? nsGroup.attributes['perm-data-plane'] : '', From 73a72977711039e9ad4248ad2e79c8cafb3a395a Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 17:08:47 -0700 Subject: [PATCH 076/109] include org param for catalog --- src/controllers/v3/OrgProductController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index cf9a0f085..2014f9b1f 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -48,12 +48,12 @@ export class OrgProductController extends Controller { /** * Get Products that are available by API across all Organizations * - * @summary Get Organization Datasets + * @summary Get Product Catalog */ - @Get('/catalog') + @Get('/{org}/catalog') @OperationId('organization-products-catalog') public async getProductCatalog( - @Request() request: any + @Path() org: string, ): Promise { const result = await this.keystone.executeGraphQL({ context: this.keystone.sudo(), From a6d267177cad5191d3066123b9de909ff23d62e3 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 17:20:26 -0700 Subject: [PATCH 077/109] v3 api tweak --- src/controllers/v3/openapi.yaml | 12 +++++++++--- src/controllers/v3/routes.ts | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index a64c0966d..d7c4a5d7e 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -2013,7 +2013,7 @@ paths: required: true schema: type: string - /organizations/catalog: + '/organizations/{org}/catalog': get: operationId: organization-products-catalog responses: @@ -2026,11 +2026,17 @@ paths: $ref: '#/components/schemas/ProductCatalog' type: array description: 'Get Products that are available by API across all Organizations' - summary: 'Get Organization Datasets' + summary: 'Get Product Catalog' tags: - 'API Directory (Administration)' security: [] - parameters: [] + parameters: + - + in: path + name: org + required: true + schema: + type: string '/organizations/{org}/products': get: operationId: organization-products diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 2904bf890..e348f26ee 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -1690,11 +1690,11 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/organizations/catalog', + app.get('/ds/api/v3/organizations/:org/catalog', async function OrgProductController_getProductCatalog(request: any, response: any, next: any) { const args = { - request: {"in":"request","name":"request","required":true,"dataType":"object"}, + org: {"in":"path","name":"org","required":true,"dataType":"string"}, }; // 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 From 568b5d90f8576bf23fde5aad06961fc0b58d66ed Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 17:34:10 -0700 Subject: [PATCH 078/109] adj catalog output --- src/controllers/v3/OrgProductController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 2014f9b1f..c39a7cc91 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -98,6 +98,7 @@ export class OrgProductController extends Controller { }, product: { name: env.product.name, + namespace: env.product.namespace, organization: { name: env.product.organization.name, }, From f51f079c8def34f296ee15845fdc1b5d9828fd21 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 17:37:08 -0700 Subject: [PATCH 079/109] adj catalog ns output --- src/controllers/v3/OrgProductController.ts | 4 +++- src/controllers/v3/openapi.yaml | 7 +++++-- src/controllers/v3/routes.ts | 4 ++-- src/controllers/v3/types-extra.ts | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index c39a7cc91..28c133bc8 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -98,6 +98,7 @@ export class OrgProductController extends Controller { }, product: { name: env.product.name, + type: env.product.type, namespace: env.product.namespace, organization: { name: env.product.organization.name, @@ -110,7 +111,7 @@ export class OrgProductController extends Controller { const nsAttributes = await getNamespaceAttributes( env.product.namespace ); - env.ns = nsAttributes; + env.namespace = nsAttributes; }); await Promise.all(promises); return output; @@ -192,6 +193,7 @@ const list = gql` } product { name + type namespace organization { name diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index d7c4a5d7e..8223b59ad 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -725,13 +725,16 @@ components: required: - name type: object + type: + type: string name: type: string required: - organization + - type - name type: object - ns: + namespace: properties: updatedAt: type: number @@ -761,7 +764,7 @@ components: - name - spec - product - - ns + - namespace type: object additionalProperties: false DraftDatasetRefID: diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index e348f26ee..2047fbb5d 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -423,8 +423,8 @@ const models: TsoaRoute.Models = { "appId": {"dataType":"string","required":true}, "name": {"dataType":"string","required":true}, "spec": {"dataType":"nestedObjectLiteral","nestedProperties":{"operations":{"dataType":"array","array":{"dataType":"refObject","ref":"ProductCatalogOperation"},"required":true},"description":{"dataType":"string","required":true},"version":{"dataType":"string","required":true},"title":{"dataType":"string","required":true}},"required":true}, - "product": {"dataType":"nestedObjectLiteral","nestedProperties":{"organization":{"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}},"required":true},"name":{"dataType":"string","required":true}},"required":true}, - "ns": {"dataType":"nestedObjectLiteral","nestedProperties":{"updatedAt":{"dataType":"double","required":true},"enabled":{"dataType":"boolean","required":true},"permDomains":{"dataType":"array","array":{"dataType":"string"},"required":true},"permDataPlane":{"dataType":"string","required":true},"orgUnit":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, + "product": {"dataType":"nestedObjectLiteral","nestedProperties":{"organization":{"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}},"required":true},"type":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, + "namespace": {"dataType":"nestedObjectLiteral","nestedProperties":{"updatedAt":{"dataType":"double","required":true},"enabled":{"dataType":"boolean","required":true},"permDomains":{"dataType":"array","array":{"dataType":"string"},"required":true},"permDataPlane":{"dataType":"string","required":true},"orgUnit":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, }, "additionalProperties": false, }, diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index cc67a75eb..3ce5031f4 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -96,11 +96,12 @@ export interface ProductCatalog { } product: { name: string; + type: string; organization: { name: string; } } - ns: { + namespace: { name: string; orgUnit: string; permDataPlane: string; From 8927f4ea50ac79083c8e6a625ef9626c6c07685b Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 20:01:45 -0700 Subject: [PATCH 080/109] fix catalog list --- src/controllers/v3/OrgProductController.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 28c133bc8..d8ad3a400 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -107,8 +107,11 @@ export class OrgProductController extends Controller { }; }); + const ctx = this.keystone.createContext(request); + const promises = output.filter((env:any) => env.product.namespace).map(async (env: any) => { const nsAttributes = await getNamespaceAttributes( + ctx, env.product.namespace ); env.namespace = nsAttributes; @@ -204,8 +207,8 @@ const list = gql` `; -async function getNamespaceAttributes(ns: string) : Promise { - const prodEnv = await getGwaProductEnvironment(this.keystone.sudo(), false); +async function getNamespaceAttributes(ctx: any, ns: string) : Promise { + const prodEnv = await getGwaProductEnvironment(ctx, false); const envConfig = prodEnv.issuerEnvConfig; const svc = new NamespaceService(envConfig.issuerUrl); From 3798d959c5620af3aef4a37d975f4ec830bc6051 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 20:21:02 -0700 Subject: [PATCH 081/109] fix catalog list --- src/controllers/v3/OrgProductController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index d8ad3a400..ad064cb62 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -54,6 +54,7 @@ export class OrgProductController extends Controller { @OperationId('organization-products-catalog') public async getProductCatalog( @Path() org: string, + @Request() request: any ): Promise { const result = await this.keystone.executeGraphQL({ context: this.keystone.sudo(), From cb8cda93ce2429485608fa0c8c3c0f81756e4a6b Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 10 Sep 2025 20:35:17 -0700 Subject: [PATCH 082/109] fix catalog list --- src/controllers/v3/OrgProductController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index ad064cb62..b70f6205d 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -56,8 +56,10 @@ export class OrgProductController extends Controller { @Path() org: string, @Request() request: any ): Promise { + const ctx = this.keystone.sudo(); + const result = await this.keystone.executeGraphQL({ - context: this.keystone.sudo(), + context: ctx, query: list, }); const envs = result.data.allEnvironments.filter( @@ -108,8 +110,6 @@ export class OrgProductController extends Controller { }; }); - const ctx = this.keystone.createContext(request); - const promises = output.filter((env:any) => env.product.namespace).map(async (env: any) => { const nsAttributes = await getNamespaceAttributes( ctx, From 86dd85e37e527a9bb104b3fa36eaba3e463e37b6 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 11 Sep 2025 16:07:52 -0700 Subject: [PATCH 083/109] adj product catalog add issuer details --- src/controllers/v3/OrgProductController.ts | 19 +++++++++++++++++++ src/controllers/v3/openapi.yaml | 10 ++++++++++ src/controllers/v3/routes.ts | 2 ++ src/controllers/v3/types-extra.ts | 4 ++++ 4 files changed, 35 insertions(+) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index b70f6205d..422190f16 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -34,6 +34,7 @@ import YAML from 'yaml'; import { getGwaProductEnvironment } from '../../services/workflow'; import { NamespaceService } from '../../services/org-groups'; import { OrgNamespace } from '../../services/org-groups/types'; +import { dynamicallySetEnvironmentDetails } from '../../services/keystone'; @injectable() @Route('/organizations') @@ -90,6 +91,16 @@ export class OrgProductController extends Controller { } } + if (env.credentialIssuer != null) { + const envDetails = JSON.parse(dynamicallySetEnvironmentDetails(env.credentialIssuer)); + const credEnv = envDetails.find((e: any) => e.environment === env.name); + + env.credentialIssuer = { + issuerUrl: credEnv?.issuerUrl, + clientId: credEnv?.clientId, + } + } + return { appId: env.appId, name: env.name, @@ -99,6 +110,7 @@ export class OrgProductController extends Controller { description: spec.info?.description || '', operations: flattenedOperations, }, + credentialIssuer: env.credentialIssuer, product: { name: env.product.name, type: env.product.type, @@ -195,6 +207,13 @@ const list = gql` spec { blob } + credentialIssuer { + name + clientId + inheritFrom { + environmentDetails + } + } product { name type diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 8223b59ad..b5e51e9d9 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -716,6 +716,16 @@ components: - version - title type: object + credentialIssuer: + properties: + clientId: + type: string + issuerUrl: + type: string + required: + - clientId + - issuerUrl + type: object product: properties: organization: diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 2047fbb5d..66860e954 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -423,6 +423,7 @@ const models: TsoaRoute.Models = { "appId": {"dataType":"string","required":true}, "name": {"dataType":"string","required":true}, "spec": {"dataType":"nestedObjectLiteral","nestedProperties":{"operations":{"dataType":"array","array":{"dataType":"refObject","ref":"ProductCatalogOperation"},"required":true},"description":{"dataType":"string","required":true},"version":{"dataType":"string","required":true},"title":{"dataType":"string","required":true}},"required":true}, + "credentialIssuer": {"dataType":"nestedObjectLiteral","nestedProperties":{"clientId":{"dataType":"string","required":true},"issuerUrl":{"dataType":"string","required":true}}}, "product": {"dataType":"nestedObjectLiteral","nestedProperties":{"organization":{"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}},"required":true},"type":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, "namespace": {"dataType":"nestedObjectLiteral","nestedProperties":{"updatedAt":{"dataType":"double","required":true},"enabled":{"dataType":"boolean","required":true},"permDomains":{"dataType":"array","array":{"dataType":"string"},"required":true},"permDataPlane":{"dataType":"string","required":true},"orgUnit":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, }, @@ -1695,6 +1696,7 @@ export function RegisterRoutes(app: express.Router) { async function OrgProductController_getProductCatalog(request: any, response: any, next: any) { const args = { org: {"in":"path","name":"org","required":true,"dataType":"string"}, + 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 diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 3ce5031f4..24108110e 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -94,6 +94,10 @@ export interface ProductCatalog { description: string; operations: ProductCatalogOperation[]; } + credentialIssuer?: { + issuerUrl: string; + clientId: string; + } product: { name: string; type: string; From e73c90ad1bac6c9ec2d6937ecee6bf85c11b329b Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 13:46:19 -0700 Subject: [PATCH 084/109] fix label saving --- src/services/workflow/org-access-request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index 2e1544302..b4f621ad6 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -138,7 +138,7 @@ export const OrgAccessRequestCreate = async ( await saveConsumerLabels( context, - app.namespace, + consumerProdEnv.product.namespace, accessRequest.serviceAccess.consumer.id, labels ); From fdd69a6404c84c53118569f46b53c462d628a610 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 14:22:57 -0700 Subject: [PATCH 085/109] fix label saving --- src/services/workflow/org-access-request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index b4f621ad6..ad26d2c4e 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -138,7 +138,7 @@ export const OrgAccessRequestCreate = async ( await saveConsumerLabels( context, - consumerProdEnv.product.namespace, + providerProdEnv.product.namespace, accessRequest.serviceAccess.consumer.id, labels ); From 20910a5864b5a9279d10577bfdd0c80f8d70bdd6 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 14:27:21 -0700 Subject: [PATCH 086/109] accomodate no org unit --- src/services/keycloak/namespace-details.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/keycloak/namespace-details.ts b/src/services/keycloak/namespace-details.ts index c32dbf917..98eb46211 100644 --- a/src/services/keycloak/namespace-details.ts +++ b/src/services/keycloak/namespace-details.ts @@ -142,7 +142,7 @@ export async function transformOrgAndOrgUnit( merged: any, getOrgAdmins: boolean ): Promise { - const orgInfo = await getOrganizationUnit(context, merged.orgUnit); + const orgInfo = merged.orgUnit ? await getOrganizationUnit(context, merged.orgUnit) : undefined; if (orgInfo) { merged['org'] = { name: orgInfo.name, title: orgInfo.title }; if (orgInfo.orgUnits) { @@ -153,7 +153,7 @@ export async function transformOrgAndOrgUnit( } } else { merged['org'] = { name: merged.org, title: merged.org }; - if (merged.orgUnits) { + if (merged.orgUnit) { merged['orgUnit'] = { name: merged.orgUnit, title: merged.orgUnit }; } } From 879f107227583143b28b04a9dd675d44f2a53e1c Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 14:39:01 -0700 Subject: [PATCH 087/109] change client name --- src/services/workflow/org-access-request.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/workflow/org-access-request.ts b/src/services/workflow/org-access-request.ts index ad26d2c4e..fa5a384a7 100644 --- a/src/services/workflow/org-access-request.ts +++ b/src/services/workflow/org-access-request.ts @@ -84,10 +84,11 @@ export const OrgAccessRequestCreate = async ( ); providerProdEnv.credentialIssuer = providerCredIssuer; - const clientName = `${formatResourceLocator( - orgMemberID, - consumerProdEnv - )} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`; + // const clientName = `${formatResourceLocator( + // orgMemberID, + // consumerProdEnv + // )} TO ${formatResourceLocator(orgMemberID, providerProdEnv)}`; + const clientName = `${consumerProdEnv.product.name} from ${orgMemberID}`; // prepare the access request const controls = { From 9974c0db036ebe1d7912fdaf2d920dfb13fa24d2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 16:18:19 -0700 Subject: [PATCH 088/109] add more to product catalog spec --- src/controllers/v3/OrgProductController.ts | 2 ++ src/controllers/v3/openapi.yaml | 6 ++++++ src/controllers/v3/routes.ts | 2 ++ src/controllers/v3/types-extra.ts | 2 ++ 4 files changed, 12 insertions(+) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 422190f16..23449740d 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -76,6 +76,8 @@ export class OrgProductController extends Controller { const op = spec.paths[path][method]; return { operationId: op.operationId, + method: method.toUpperCase(), + path, summary: op.summary || '', scopes: (op.security && op.security[0] && op.security[0]['bearer_auth']) ? op.security[0]['bearer_auth'] : [], }; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index b5e51e9d9..8b56f4580 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -680,6 +680,10 @@ components: properties: operationId: type: string + method: + type: string + path: + type: string summary: type: string scopes: @@ -688,6 +692,8 @@ components: type: array required: - operationId + - method + - path - summary - scopes type: object diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 66860e954..a8027307e 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -411,6 +411,8 @@ const models: TsoaRoute.Models = { "dataType": "refObject", "properties": { "operationId": {"dataType":"string","required":true}, + "method": {"dataType":"string","required":true}, + "path": {"dataType":"string","required":true}, "summary": {"dataType":"string","required":true}, "scopes": {"dataType":"array","array":{"dataType":"string"},"required":true}, }, diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts index 24108110e..a94f43eb6 100644 --- a/src/controllers/v3/types-extra.ts +++ b/src/controllers/v3/types-extra.ts @@ -81,6 +81,8 @@ export interface OrgAccessRequest { export interface ProductCatalogOperation { operationId: string; + method: string; + path: string; summary: string; scopes: string[]; } From 1513fff7bbbe338aa507e1b958b51212be95a9fb Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 16:56:58 -0700 Subject: [PATCH 089/109] try and fix subjectDN not working --- src/services/keycloak/client-registration-service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index 67a444851..7742c7342 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -107,10 +107,8 @@ export class KeycloakClientRegistrationService { name, clientId, consentRequired: true, - attributes: { - 'x509.subjectdn': subjectDn - } }); + body.attributes['x509.subjectdn'] = subjectDn; break; case ClientAuthenticator.SharedIdP: body = Object.assign(JSON.parse(clientTemplateSharedIdP), { From 692f378911d2c5ceb955687822646b7e78f2bdb1 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 12 Sep 2025 16:58:17 -0700 Subject: [PATCH 090/109] try and fix subjectDN not working --- src/services/keycloak/client-registration-service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index 7742c7342..d3a331b28 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -107,8 +107,11 @@ export class KeycloakClientRegistrationService { name, clientId, consentRequired: true, + attributes: { + "x509.allow.regex.pattern.comparison": "false", + "x509.subjectdn": subjectDn, + } }); - body.attributes['x509.subjectdn'] = subjectDn; break; case ClientAuthenticator.SharedIdP: body = Object.assign(JSON.parse(clientTemplateSharedIdP), { From 1d1f32d6d62d0a5a61a7ea164b8a2abe5c043a48 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 25 Sep 2025 20:41:45 -0700 Subject: [PATCH 091/109] boost speed on catalog call --- src/controllers/v3/OrgProductController.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/controllers/v3/OrgProductController.ts b/src/controllers/v3/OrgProductController.ts index 23449740d..912a42f16 100644 --- a/src/controllers/v3/OrgProductController.ts +++ b/src/controllers/v3/OrgProductController.ts @@ -109,6 +109,7 @@ export class OrgProductController extends Controller { spec: { title: spec.info?.title || '', version: spec.info?.version || '', + summary: spec.info?.summary || '', description: spec.info?.description || '', operations: flattenedOperations, }, @@ -124,9 +125,15 @@ export class OrgProductController extends Controller { }; }); + const prodEnv = await getGwaProductEnvironment(ctx, false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + const promises = output.filter((env:any) => env.product.namespace).map(async (env: any) => { const nsAttributes = await getNamespaceAttributes( - ctx, + svc, env.product.namespace ); env.namespace = nsAttributes; @@ -229,11 +236,6 @@ const list = gql` `; -async function getNamespaceAttributes(ctx: any, ns: string) : Promise { - const prodEnv = await getGwaProductEnvironment(ctx, false); - const envConfig = prodEnv.issuerEnvConfig; - - const svc = new NamespaceService(envConfig.issuerUrl); - await svc.login(envConfig.clientId, envConfig.clientSecret); +async function getNamespaceAttributes(svc: NamespaceService, ns: string) : Promise { return await svc.getNamespaceOrganizationDetails(ns); } From 48faf9c41874df3887c7a8b712c1e806ecb46db2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 15:34:47 -0800 Subject: [PATCH 092/109] new sdx controller --- src/controllers/v3/SDXController.ts | 61 ++++++++ src/controllers/v3/openapi.yaml | 133 ++++++++++++++++ src/controllers/v3/routes.ts | 90 +++++++++++ src/services/gwaapi/gwa-service.ts | 15 ++ src/services/identifiers.ts | 4 + src/services/keycloak/group-service.ts | 2 +- src/services/sdx/edge-servers.ts | 14 ++ src/services/sdx/gateway-patterns.ts | 180 ++++++++++++++++++++++ src/services/sdx/member-id.ts | 14 ++ src/services/sdx/sdx-catalog.ts | 204 +++++++++++++++++++++++++ src/test/integrated/sdx/getcatalog.ts | 73 +++++++++ 11 files changed, 789 insertions(+), 1 deletion(-) create mode 100644 src/controllers/v3/SDXController.ts create mode 100644 src/services/sdx/edge-servers.ts create mode 100644 src/services/sdx/gateway-patterns.ts create mode 100644 src/services/sdx/member-id.ts create mode 100644 src/services/sdx/sdx-catalog.ts create mode 100644 src/test/integrated/sdx/getcatalog.ts diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts new file mode 100644 index 000000000..3b621b926 --- /dev/null +++ b/src/controllers/v3/SDXController.ts @@ -0,0 +1,61 @@ +import { + Controller, + Request, + OperationId, + Get, + Put, + Path, + Route, + Security, + Body, + Tags, + FormField, + UploadedFile, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecords, + getRecords, + parseJsonString, + removeEmpty, + removeKeys, +} from '../../batch/feed-worker'; +import { GatewayRoute } from './types'; +import { PublishResult } from './types-extra'; +import { CatalogEntry, GetCatalog } from '../../services/sdx/sdx-catalog'; +import { + GatewayPatternConfig, + GetConfigUsingPattern, +} from '../../services/sdx/gateway-patterns'; + +@injectable() +@Route('/sdx') +@Tags('SDX') +export class SDXController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + @Put('/{gatewayId}/config-from-pattern') + @OperationId('get-config-from-sdx-pattern') + @Security('jwt', []) + public async put( + @Path() gatewayId: string, + @Body() body: GatewayPatternConfig + ): Promise { + const ctx = this.keystone.createContext(request); + return await GetConfigUsingPattern(ctx, body); + } + + @Get() + @OperationId('get-catalog') + @Security('jwt', []) + public async getCatalog(@Request() request: any): Promise { + const ctx = this.keystone.createContext(request); + + return await GetCatalog(ctx); + } +} diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 8b56f4580..28d2ae36a 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -873,6 +873,94 @@ components: approval: false flow: public appId: '00000000' + Record_string.string_: + properties: {} + type: object + description: 'Construct a type with a set of properties K of type T' + GatewayPatternConfig: + properties: + pattern: + type: string + locator: + type: string + parameters: + $ref: '#/components/schemas/Record_string.string_' + required: + - pattern + - locator + - parameters + type: object + additionalProperties: false + CatalogEntry: + properties: + id: + type: string + locator: + type: string + product: + properties: + namespace: + type: string + type: + type: string + name: + type: string + required: + - namespace + - type + - name + type: object + organization: + properties: + orgUnit: + type: string + name: + type: string + required: + - name + type: object + gateway: + properties: + permissions: + properties: + domains: + items: {type: string} + type: array + dataPlane: + items: {type: string} + type: array + required: + - domains + - dataPlane + type: object + name: + type: string + required: + - permissions + - name + type: object + edgeServer: + properties: + dn: + type: string + host: + type: string + required: + - dn + - host + type: object + hasSpec: + type: boolean + required: + - id + - locator + - product + - organization + - gateway + - edgeServer + - hasSpec + type: object + additionalProperties: false securitySchemes: jwt: type: oauth2 @@ -2249,6 +2337,51 @@ paths: schema: default: false type: boolean + '/sdx/{gatewayId}/config-from-pattern': + put: + operationId: get-config-from-sdx-pattern + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - SDX + security: + - + jwt: [] + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayPatternConfig' + /sdx: + get: + operationId: get-catalog + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/CatalogEntry' + type: array + tags: + - SDX + security: + - + jwt: [] + parameters: [] servers: - url: /ds/api/v3 diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index a8027307e..234a873dd 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -32,6 +32,8 @@ import { OrgProductController } from './OrgProductController'; import { OrgRoleController } from './OrgRoleController'; // 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 { ProductController } from './ProductController'; +// 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 { SDXController } from './SDXController'; import { expressAuthentication } from './../../auth/auth-tsoa'; // @ts-ignore - no great way to install types from subpackage const promiseAny = require('promise.any'); @@ -479,6 +481,35 @@ 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 + "Record_string.string_": { + "dataType": "refAlias", + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"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 + "GatewayPatternConfig": { + "dataType": "refObject", + "properties": { + "pattern": {"dataType":"string","required":true}, + "locator": {"dataType":"string","required":true}, + "parameters": {"ref":"Record_string.string_","required":true}, + }, + "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 + "CatalogEntry": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "locator": {"dataType":"string","required":true}, + "product": {"dataType":"nestedObjectLiteral","nestedProperties":{"namespace":{"dataType":"string","required":true},"type":{"dataType":"string","required":true},"name":{"dataType":"string","required":true}},"required":true}, + "organization": {"dataType":"nestedObjectLiteral","nestedProperties":{"orgUnit":{"dataType":"string"},"name":{"dataType":"string","required":true}},"required":true}, + "gateway": {"dataType":"nestedObjectLiteral","nestedProperties":{"permissions":{"dataType":"nestedObjectLiteral","nestedProperties":{"domains":{"dataType":"array","array":{"dataType":"string"},"required":true},"dataPlane":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true},"name":{"dataType":"string","required":true}},"required":true}, + "edgeServer": {"dataType":"nestedObjectLiteral","nestedProperties":{"dn":{"dataType":"string","required":true},"host":{"dataType":"string","required":true}},"required":true}, + "hasSpec": {"dataType":"boolean","required":true}, + }, + "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 }; const validationService = new ValidationService(models); @@ -1934,6 +1965,65 @@ export function RegisterRoutes(app: express.Router) { } }); // 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/v3/sdx/:gatewayId/config-from-pattern', + authenticateMiddleware([{"jwt":[]}]), + + async function SDXController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"GatewayPatternConfig"}, + }; + + // 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(SDXController); + 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/v3/sdx', + authenticateMiddleware([{"jwt":[]}]), + + async function SDXController_getCatalog(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(SDXController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getCatalog.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 // 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 diff --git a/src/services/gwaapi/gwa-service.ts b/src/services/gwaapi/gwa-service.ts index 86078bdf5..ca029b53e 100644 --- a/src/services/gwaapi/gwa-service.ts +++ b/src/services/gwaapi/gwa-service.ts @@ -33,4 +33,19 @@ export class GWAService { }, }).then(checkStatus); } + + public async getGatewayConfigUsingPattern(ns: string, payload: any) { + const url = `${this.gwaUrl}/v2/namespaces/${ns}/gateway/pattern-output`; + logger.debug('[getGatewayConfigUsingPattern] ns=%s', ns); + + return await fetch(url, { + method: 'put', + body: JSON.stringify({ document: payload }), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(checkStatus) + .then((res) => res.json()); + } } diff --git a/src/services/identifiers.ts b/src/services/identifiers.ts index 06e8f7d17..706a753a1 100644 --- a/src/services/identifiers.ts +++ b/src/services/identifiers.ts @@ -24,4 +24,8 @@ export function newNamespaceID(): string { return 'gw-' + uuidv4().replace(/-/g, '').toLowerCase().substring(0, 5); } +export function newJWKID(): string { + return uuidv4().replace(/-/g, '').toLowerCase().substring(0, 6); +} + export const newGatewayID = newNamespaceID; diff --git a/src/services/keycloak/group-service.ts b/src/services/keycloak/group-service.ts index 99014aee7..19ff9aed1 100644 --- a/src/services/keycloak/group-service.ts +++ b/src/services/keycloak/group-service.ts @@ -131,7 +131,7 @@ export class KeycloakGroupService { max: 500, briefRepresentation, }); - logger.debug('[search] %j', result); + logger.debug('[search] %j', result.length); return result; } diff --git a/src/services/sdx/edge-servers.ts b/src/services/sdx/edge-servers.ts new file mode 100644 index 000000000..863e20fbe --- /dev/null +++ b/src/services/sdx/edge-servers.ts @@ -0,0 +1,14 @@ +export async function LookupEdgeServer(host: string) { + const servers = await fetch( + 'https://sdx-beta-api-gov-bc-ca-lab.dev.api.gov.bc.ca/api/rd/access-points', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + const serverData = await servers.json(); + return serverData.filter((server: any) => server.host === host).pop(); +} diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts new file mode 100644 index 000000000..10a0c9575 --- /dev/null +++ b/src/services/sdx/gateway-patterns.ts @@ -0,0 +1,180 @@ +/* + +- this service supports the controller for gateway pattern based configuration +- it uses the "catalog" to prepare the templated parameters before sending to +- the gwa-api service for generating the config +*/ + +import { GWAService } from '../gwaapi'; +import { logger } from '../../logger'; +import { CatalogEntry, GetCatalog } from './sdx-catalog'; +import { newEnvironmentID, newJWKID } from '../identifiers'; + +export interface GatewayPatternConfig { + pattern: string; + locator: string; + parameters: Record; +} + +export async function GetConfigUsingPattern( + ctx: any, + inputs: GatewayPatternConfig +): Promise { + const catalog = await GetCatalog(ctx); + const entry = catalog.find((e) => e.locator === inputs.locator); + if (!entry) { + throw new Error( + `GetConfigUsingPattern: unable to find catalog entry for locator ${inputs.locator}` + ); + } + if (inputs.pattern.startsWith('sdx-keys-')) { + expectRequiredParams(inputs.parameters, ['public_key_pem']); + return await evalKeysPattern( + inputs.pattern, + entry, + inputs.parameters['public_key_pem'] + ); + } else if (inputs.pattern.startsWith('sdx-p2p-consumer-')) { + expectRequiredParams(inputs.parameters, ['provider', 'req_id']); + const provider = catalog.find( + (e) => e.locator === inputs.parameters['provider'] + ); + const reqId = inputs.parameters['req_id']; + return await evalConsumerPattern(inputs.pattern, reqId, entry, provider); + } else if (inputs.pattern.startsWith('sdx-p2p-provider-')) { + expectRequiredParams(inputs.parameters, [ + 'consumer', + 'req_id', + 'upstream_uri', + ]); + const consumer = catalog.find( + (e) => e.locator === inputs.parameters['consumer'] + ); + const reqId = inputs.parameters['req_id']; + const upstreamUri = inputs.parameters['upstream_uri']; + return await evalProviderPattern( + inputs.pattern, + reqId, + upstreamUri, + entry, + consumer + ); + } else { + throw new Error( + `GetConfigUsingPattern: unsupported pattern ${inputs.pattern}` + ); + } +} + +async function evalKeysPattern( + pattern: string, + entry: CatalogEntry, + publicKeyPem: string +) { + const gwa = new GWAService(process.env.GWA_API_URL); + + const kid = `urn:ca:bc:sdx:service:${entry.locator.toLowerCase()}:${newJWKID()}`; + const keyName = `SDX.${entry.locator}:0`; + const result = await gwa.getGatewayConfigUsingPattern(entry.gateway.name, { + pattern: pattern, + kid, + key_name: keyName, + ns_qualifier: `KEYS-${entry.product.name}`, + public_key_pem: publicKeyPem, + }); + return result; +} + +async function evalConsumerPattern( + pattern: string, + reqId: string, + entry: CatalogEntry, + provider: CatalogEntry +) { + const gwa = new GWAService(process.env.GWA_API_URL); + + const kid = `urn:ca:bc:sdx:service:${entry.locator.toLowerCase()}:${newJWKID()}`; + const keyName = `SDX.${entry.locator}:0`; + const result = await gwa.getGatewayConfigUsingPattern(entry.gateway.name, { + pattern, + consumer_uri: entry.locator, + gateway: entry.gateway.name, + ns_qualifier: `AP-C-REQ-${reqId}`, + route_host: entry.edgeServer.host, + route_path: `/${provider.locator}`, + service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, + upstream_uri: `https://${provider.edgeServer.host}`, + }); + return result; +} + +async function evalProviderPattern( + pattern: string, + reqId: string, + upstreamUri: string, + entry: CatalogEntry, + consumer: CatalogEntry +) { + const gwa = new GWAService(process.env.GWA_API_URL); + + const result = await gwa.getGatewayConfigUsingPattern(entry.gateway.name, { + pattern, + consumer_uri: consumer.locator, + gateway: entry.gateway.name, + mtls_allow_list: `"${entry.edgeServer.dn}"`, + ns_qualifier: `AP-P-REQ-${reqId}`, + route_host: entry.edgeServer.host, + route_path: `/${entry.locator}`, + service_name: `AP-P-REQ-${reqId}-${entry.product.name}`, + upstream_uri: upstreamUri, + }); + return result; +} + +function expectRequiredParams( + provided: Record, + required: string[] +) { + for (const param of required) { + if (!provided[param]) { + throw new Error(`missing required parameter: ${param}`); + } + } +} + +/* +CONSUMER: + +consumer_client_id: ap-gw-31a33-default-dev +consumer_uri: DEV.MIN.CITZ.SINGLE-DIGITAL-GW +gateway: gw-31a33 +mtls_allow_list: '"CN=sdxgov.edge.sdx"' +ns_qualifier: AP-P-REQ-229 +openid_audience: ap-gw-31a33-default-dev +openid_issuer: https://sdx-authz-apps-gov-bc-ca-lab.apps.gov.bc.ca/auth/realms/sdx +openid_scope: '' +pattern: sdx-p2p-provider-r1 +route_host: ministryofpuppiesandkittens.xyz +route_path: /DEV/MIN/PUKI/TOYS +service_name: AP-P-REQ-229-TOYS +upstream_uri: https://httpbun.com +*/ + +/* +PROVIDER: + +consumer_client_id: ap-gw-31a33-default-dev +consumer_uri: DEV.MIN.CITZ.SINGLE-DIGITAL-GW +edge_kid: urn:ca:bc:sdx:edge:sdxgov:0 +gateway: gw-8aa16 +mtls_allow_list: '' +ns_qualifier: AP-C-REQ-229 +openid_audience: ap-gw-31a33-default-dev +openid_issuer: https://sdx-authz-apps-gov-bc-ca-lab.apps.gov.bc.ca/auth/realms/sdx +openid_scope: '' +pattern: sdx-p2p-consumer-r1 +route_host: sdx.gov.bc.ca +route_path: /DEV/MIN/PUKI/TOYS +service_name: AP-C-REQ-229-TOYS +upstream_uri: https://ministryofpuppiesandkittens.xyz +*/ diff --git a/src/services/sdx/member-id.ts b/src/services/sdx/member-id.ts new file mode 100644 index 000000000..c1b46c57d --- /dev/null +++ b/src/services/sdx/member-id.ts @@ -0,0 +1,14 @@ +export async function LookupMemberOrganization(id: string) { + const member = await fetch( + 'https://sdx-beta-api-gov-bc-ca-lab.dev.api.gov.bc.ca/api/rd/members', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + const memberData = await member.json(); + return memberData.filter((member: any) => member.id === id).pop(); +} diff --git a/src/services/sdx/sdx-catalog.ts b/src/services/sdx/sdx-catalog.ts new file mode 100644 index 000000000..571546445 --- /dev/null +++ b/src/services/sdx/sdx-catalog.ts @@ -0,0 +1,204 @@ +import YAML from 'yaml'; +import { gql } from 'graphql-request'; +import { Environment } from '../keystone/types'; +import { NamespaceService } from '../org-groups'; +import { OrgNamespace } from '../org-groups/types'; +import { getGwaProductEnvironment } from '../workflow'; +import { dynamicallySetEnvironmentDetails } from '../keystone'; +import { LookupMemberOrganization } from './member-id'; +import { LookupEdgeServer } from './edge-servers'; + +export interface CatalogEntry { + id: string; + locator: string; + product: { + name: string; + type: string; + namespace: string; + }; + organization: { + name: string; + orgUnit?: string; + }; + gateway: { + name: string; + permissions: { + dataPlane: string[]; + domains: string[]; + }; + }; + edgeServer: { + host: string; + dn: string; + }; + hasSpec: boolean; +} + +export async function GetCatalog(ctx: any): Promise { + const result = await ctx.executeGraphQL({ + context: ctx, + query: list, + }); + const envs = result.data.allEnvironments.filter( + (e: Environment) => e.product.organization != null + ); + + const output = envs.map((env: any) => { + //const spec = env.spec?.blob ? parseSpec(env.spec?.blob) : undefined; + const spec: any = undefined; + + if (env.credentialIssuer != null) { + const envDetails = JSON.parse( + dynamicallySetEnvironmentDetails(env.credentialIssuer) + ); + const credEnv = envDetails.find((e: any) => e.environment === env.name); + + env.credentialIssuer = { + issuerUrl: credEnv?.issuerUrl, + clientId: credEnv?.clientId, + }; + } + + const catalogEntry = { + id: env.appId, + environment: env.name, + product: { + name: env.product.name, + }, + organization: { + name: env.product.organization.name, + orgUnit: env.product.organization.orgUnit || undefined, + }, + gateway: { + name: env.product.namespace, + }, + hasSpec: env.spec?.id ? true : false, + }; + return catalogEntry; + }); + + const prodEnv = await getGwaProductEnvironment(ctx, false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + + const promises = output + .filter((env: any) => env.gateway.name) + .map(async (env: any) => { + const member = await LookupMemberOrganization(env.organization.name); + env.locator = [ + `${env.environment.toUpperCase()}`, + `${member.member_class}`, + `${member.member_id}`, + `${env.product.name}`, + ] + .join('.') + .toUpperCase(); + + const nsAttributes = await getNamespaceAttributes(svc, env.gateway.name); + env.gateway.permissions = { + dataPlane: nsAttributes.permDataPlane, + domains: nsAttributes.permDomains, + }; + + const edgeServer = await LookupEdgeServer( + env.gateway.permissions.domains[0] + ); + env.edgeServer = { + host: edgeServer.host, + dn: edgeServer.dn, + }; + }); + await Promise.all(promises); + return output; +} + +async function parseSpec(specBlob: string) { + const spec = YAML.parse(specBlob); + + const operations = + spec?.paths && + Object.keys(spec.paths).map((path) => { + return Object.keys(spec.paths[path]).map((method) => { + const op = spec.paths[path][method]; + return { + operationId: op.operationId, + method: method.toUpperCase(), + path, + summary: op.summary || '', + scopes: + op.security && op.security[0] && op.security[0]['bearer_auth'] + ? op.security[0]['bearer_auth'] + : [], + }; + }); + }); + + const flattenedOperations = []; + if (operations) { + for (const opList of operations) { + for (const op of opList) { + flattenedOperations.push(op); + } + } + } +} + +async function getNamespaceAttributes( + svc: NamespaceService, + ns: string +): Promise { + return await svc.getNamespaceOrganizationDetails(ns); +} + +const list = gql` + query OrgProductCatalog { + allEnvironments { + appId + name + spec { + id + blob + } + credentialIssuer { + name + clientId + inheritFrom { + environmentDetails + } + } + product { + name + type + namespace + organization { + name + } + } + } + } +`; + +/* + return { + appId: env.appId, + name: env.name, + spec: { + title: spec.info?.title || '', + version: spec.info?.version || '', + summary: spec.info?.summary || '', + description: spec.info?.description || '', + operations: flattenedOperations, + }, + credentialIssuer: env.credentialIssuer, + product: { + name: env.product.name, + type: env.product.type, + namespace: env.product.namespace, + organization: { + name: env.product.organization.name, + }, + }, + }; +*/ diff --git a/src/test/integrated/sdx/getcatalog.ts b/src/test/integrated/sdx/getcatalog.ts new file mode 100644 index 000000000..f6d19b797 --- /dev/null +++ b/src/test/integrated/sdx/getcatalog.ts @@ -0,0 +1,73 @@ +/* +Wire up directly with Keycloak and use the Services +To run: +npm run ts-build +npm run ts-watch +node dist/test/integrated/sdx/getcatalog.js +*/ + +import { o } from '../util'; +import InitKeystone from '../keystonejs/init'; +import { GetCatalog } from '../../../services/sdx/sdx-catalog'; +import { GetConfigUsingPattern } from '../../../services/sdx/gateway-patterns'; + +(async () => { + const keystone = await InitKeystone(); + console.log('K = ' + keystone); + + const ns = 'platform'; + const skipAccessControl = false; + + const identity = { + id: null, + username: 'sample_username', + namespace: ns, + roles: JSON.stringify(['api-owner']), + scopes: [], + userId: null, + } as any; + + const ctx = keystone.createContext({ + skipAccessControl, + authentication: { item: identity }, + }); + + // o(await getOrganizations(ctx)); + + o(await GetCatalog(ctx)); + + // o( + // await GetConfigUsingPattern(ctx, { + // pattern: 'sdx-keys-r1', + // locator: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', + // parameters: { + // public_key_pem: 'sample-public-key-pem', + // }, + // }) + // ); + + o( + await GetConfigUsingPattern(ctx, { + pattern: 'sdx-p2p-consumer-pub-r1', + locator: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', + parameters: { + provider: 'DEV.MIN.PUKI.TOYS', + req_id: '12345', + }, + }) + ); + + o( + await GetConfigUsingPattern(ctx, { + pattern: 'sdx-p2p-provider-pub-r1', + locator: 'DEV.MIN.PUKI.TOYS', + parameters: { + consumer: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', + req_id: '12345', + upstream_uri: 'https://httpbun.com', + }, + }) + ); + + await keystone.disconnect(); +})(); From e2cd0aba55e66b252e071405be612980fa598d56 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 15:39:42 -0800 Subject: [PATCH 093/109] fix controller bug --- src/controllers/v3/SDXController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts index 3b621b926..bbd718a05 100644 --- a/src/controllers/v3/SDXController.ts +++ b/src/controllers/v3/SDXController.ts @@ -44,7 +44,8 @@ export class SDXController extends Controller { @Security('jwt', []) public async put( @Path() gatewayId: string, - @Body() body: GatewayPatternConfig + @Body() body: GatewayPatternConfig, + @Request() request: any ): Promise { const ctx = this.keystone.createContext(request); return await GetConfigUsingPattern(ctx, body); From 7ddf3d1d0666d97e48b90614c7f44624549f1a97 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 20:37:56 -0800 Subject: [PATCH 094/109] allow for any params --- src/controllers/v3/SDXController.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts index bbd718a05..54fa35d00 100644 --- a/src/controllers/v3/SDXController.ts +++ b/src/controllers/v3/SDXController.ts @@ -24,10 +24,13 @@ import { import { GatewayRoute } from './types'; import { PublishResult } from './types-extra'; import { CatalogEntry, GetCatalog } from '../../services/sdx/sdx-catalog'; -import { - GatewayPatternConfig, - GetConfigUsingPattern, -} from '../../services/sdx/gateway-patterns'; +import { GetConfigUsingPattern } from '../../services/sdx/gateway-patterns'; + +interface GatewayPatternConfigRequest { + pattern: string; + locator: string; + parameters: any; +} @injectable() @Route('/sdx') @@ -44,7 +47,7 @@ export class SDXController extends Controller { @Security('jwt', []) public async put( @Path() gatewayId: string, - @Body() body: GatewayPatternConfig, + @Body() body: GatewayPatternConfigRequest, @Request() request: any ): Promise { const ctx = this.keystone.createContext(request); From 3a1463c0d92125cac9e5abcf010a6b03b99ae166 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 22:00:26 -0800 Subject: [PATCH 095/109] add support for delete and change pattern payload --- src/services/gwaapi/gwa-service.ts | 8 +- src/services/sdx/gateway-patterns.ts | 130 ++++++++++++++++++--------- 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/src/services/gwaapi/gwa-service.ts b/src/services/gwaapi/gwa-service.ts index ca029b53e..d177059f0 100644 --- a/src/services/gwaapi/gwa-service.ts +++ b/src/services/gwaapi/gwa-service.ts @@ -34,13 +34,17 @@ export class GWAService { }).then(checkStatus); } - public async getGatewayConfigUsingPattern(ns: string, payload: any) { + public async getGatewayConfigUsingPattern( + ns: string, + deleteFlag: boolean, + payload: any + ) { const url = `${this.gwaUrl}/v2/namespaces/${ns}/gateway/pattern-output`; logger.debug('[getGatewayConfigUsingPattern] ns=%s', ns); return await fetch(url, { method: 'put', - body: JSON.stringify({ document: payload }), + body: JSON.stringify({ delete: deleteFlag, document: payload }), headers: { 'Content-Type': 'application/json', }, diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index 10a0c9575..121e742cb 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -12,7 +12,7 @@ import { newEnvironmentID, newJWKID } from '../identifiers'; export interface GatewayPatternConfig { pattern: string; - locator: string; + delete?: boolean; parameters: Record; } @@ -21,42 +21,73 @@ export async function GetConfigUsingPattern( inputs: GatewayPatternConfig ): Promise { const catalog = await GetCatalog(ctx); - const entry = catalog.find((e) => e.locator === inputs.locator); - if (!entry) { - throw new Error( - `GetConfigUsingPattern: unable to find catalog entry for locator ${inputs.locator}` - ); - } if (inputs.pattern.startsWith('sdx-keys-')) { - expectRequiredParams(inputs.parameters, ['public_key_pem']); + expectRequiredParams(inputs.parameters, ['locator', 'public_key_pem']); + const entry = catalog.find( + (e) => e.locator === inputs.parameters['locator'] + ); + if (!entry) { + throw new Error( + `GetConfigUsingPattern: unable to find catalog entry for locator ${inputs.parameters['locator']}` + ); + } + return await evalKeysPattern( inputs.pattern, + inputs.delete === true, entry, inputs.parameters['public_key_pem'] ); } else if (inputs.pattern.startsWith('sdx-p2p-consumer-')) { - expectRequiredParams(inputs.parameters, ['provider', 'req_id']); + expectRequiredParams(inputs.parameters, ['consumer', 'provider', 'req_id']); const provider = catalog.find( (e) => e.locator === inputs.parameters['provider'] ); const reqId = inputs.parameters['req_id']; - return await evalConsumerPattern(inputs.pattern, reqId, entry, provider); + const consumer = catalog.find( + (e) => e.locator === inputs.parameters['consumer'] + ); + if (!consumer) { + throw new Error( + `GetConfigUsingPattern: unable to find catalog entry for locator ${inputs.parameters['locator']}` + ); + } + + return await evalConsumerPattern( + inputs.pattern, + inputs.delete === true, + reqId, + consumer, + provider + ); } else if (inputs.pattern.startsWith('sdx-p2p-provider-')) { expectRequiredParams(inputs.parameters, [ 'consumer', + 'provider', 'req_id', 'upstream_uri', ]); + const provider = catalog.find( + (e) => e.locator === inputs.parameters['provider'] + ); + if (!provider) { + throw new Error( + `GetConfigUsingPattern: unable to find catalog entry for locator ${inputs.parameters['provider']}` + ); + } + const consumer = catalog.find( (e) => e.locator === inputs.parameters['consumer'] ); const reqId = inputs.parameters['req_id']; + const upstreamUri = inputs.parameters['upstream_uri']; return await evalProviderPattern( inputs.pattern, + inputs.delete === true, reqId, upstreamUri, - entry, + provider, consumer ); } else { @@ -68,6 +99,7 @@ export async function GetConfigUsingPattern( async function evalKeysPattern( pattern: string, + deleteFlag: boolean, entry: CatalogEntry, publicKeyPem: string ) { @@ -75,59 +107,71 @@ async function evalKeysPattern( const kid = `urn:ca:bc:sdx:service:${entry.locator.toLowerCase()}:${newJWKID()}`; const keyName = `SDX.${entry.locator}:0`; - const result = await gwa.getGatewayConfigUsingPattern(entry.gateway.name, { - pattern: pattern, - kid, - key_name: keyName, - ns_qualifier: `KEYS-${entry.product.name}`, - public_key_pem: publicKeyPem, - }); + const result = await gwa.getGatewayConfigUsingPattern( + entry.gateway.name, + deleteFlag, + { + pattern: pattern, + kid, + key_name: keyName, + ns_qualifier: `KEYS-${entry.product.name}`, + public_key_pem: publicKeyPem, + } + ); return result; } async function evalConsumerPattern( pattern: string, + deleteFlag: boolean, reqId: string, - entry: CatalogEntry, + consumer: CatalogEntry, provider: CatalogEntry ) { const gwa = new GWAService(process.env.GWA_API_URL); - const kid = `urn:ca:bc:sdx:service:${entry.locator.toLowerCase()}:${newJWKID()}`; - const keyName = `SDX.${entry.locator}:0`; - const result = await gwa.getGatewayConfigUsingPattern(entry.gateway.name, { - pattern, - consumer_uri: entry.locator, - gateway: entry.gateway.name, - ns_qualifier: `AP-C-REQ-${reqId}`, - route_host: entry.edgeServer.host, - route_path: `/${provider.locator}`, - service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, - upstream_uri: `https://${provider.edgeServer.host}`, - }); + const result = await gwa.getGatewayConfigUsingPattern( + consumer.gateway.name, + deleteFlag, + { + pattern, + consumer_uri: consumer.locator, + gateway: consumer.gateway.name, + ns_qualifier: `AP-C-REQ-${reqId}`, + route_host: consumer.edgeServer.host, + route_path: `/${provider.locator}`, + service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, + upstream_uri: `https://${provider.edgeServer.host}`, + } + ); return result; } async function evalProviderPattern( pattern: string, + deleteFlag: boolean, reqId: string, upstreamUri: string, - entry: CatalogEntry, + provider: CatalogEntry, consumer: CatalogEntry ) { const gwa = new GWAService(process.env.GWA_API_URL); - const result = await gwa.getGatewayConfigUsingPattern(entry.gateway.name, { - pattern, - consumer_uri: consumer.locator, - gateway: entry.gateway.name, - mtls_allow_list: `"${entry.edgeServer.dn}"`, - ns_qualifier: `AP-P-REQ-${reqId}`, - route_host: entry.edgeServer.host, - route_path: `/${entry.locator}`, - service_name: `AP-P-REQ-${reqId}-${entry.product.name}`, - upstream_uri: upstreamUri, - }); + const result = await gwa.getGatewayConfigUsingPattern( + provider.gateway.name, + deleteFlag, + { + pattern, + consumer_uri: consumer.locator, + gateway: provider.gateway.name, + mtls_allow_list: `"${provider.edgeServer.dn}"`, + ns_qualifier: `AP-P-REQ-${reqId}`, + route_host: provider.edgeServer.host, + route_path: `/${provider.locator}`, + service_name: `AP-P-REQ-${reqId}-${provider.product.name}`, + upstream_uri: upstreamUri, + } + ); return result; } From ac8b478adf8e75a4360282f8096ded76ca865c85 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 22:04:35 -0800 Subject: [PATCH 096/109] upd catalog --- src/test/integrated/sdx/getcatalog.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/integrated/sdx/getcatalog.ts b/src/test/integrated/sdx/getcatalog.ts index f6d19b797..34d999015 100644 --- a/src/test/integrated/sdx/getcatalog.ts +++ b/src/test/integrated/sdx/getcatalog.ts @@ -49,10 +49,10 @@ import { GetConfigUsingPattern } from '../../../services/sdx/gateway-patterns'; o( await GetConfigUsingPattern(ctx, { pattern: 'sdx-p2p-consumer-pub-r1', - locator: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', parameters: { - provider: 'DEV.MIN.PUKI.TOYS', req_id: '12345', + consumer: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', + provider: 'DEV.MIN.PUKI.TOYS', }, }) ); @@ -60,10 +60,10 @@ import { GetConfigUsingPattern } from '../../../services/sdx/gateway-patterns'; o( await GetConfigUsingPattern(ctx, { pattern: 'sdx-p2p-provider-pub-r1', - locator: 'DEV.MIN.PUKI.TOYS', parameters: { - consumer: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', req_id: '12345', + consumer: 'DEV.MIN.CITZ.SINGLE-DIGITAL-GW', + provider: 'DEV.MIN.PUKI.TOYS', upstream_uri: 'https://httpbun.com', }, }) From 522473c2890d02fd07800e797957637736c15857 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 22:22:19 -0800 Subject: [PATCH 097/109] fix sdx controller --- src/controllers/v3/SDXController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts index 54fa35d00..0d5ce02e4 100644 --- a/src/controllers/v3/SDXController.ts +++ b/src/controllers/v3/SDXController.ts @@ -28,7 +28,6 @@ import { GetConfigUsingPattern } from '../../services/sdx/gateway-patterns'; interface GatewayPatternConfigRequest { pattern: string; - locator: string; parameters: any; } From b18486f06170a448f369c0a14a28a4d993910385 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 22:33:05 -0800 Subject: [PATCH 098/109] fix sdx controller --- src/controllers/v3/SDXController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts index 0d5ce02e4..4f63bd5e5 100644 --- a/src/controllers/v3/SDXController.ts +++ b/src/controllers/v3/SDXController.ts @@ -28,6 +28,7 @@ import { GetConfigUsingPattern } from '../../services/sdx/gateway-patterns'; interface GatewayPatternConfigRequest { pattern: string; + delete?: boolean; parameters: any; } From 0241d358030d8511bb9d36215563d7755b0ae276 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 3 Dec 2025 22:47:58 -0800 Subject: [PATCH 099/109] fix delete qualifier --- src/services/gwaapi/gwa-service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/gwaapi/gwa-service.ts b/src/services/gwaapi/gwa-service.ts index d177059f0..cecd95169 100644 --- a/src/services/gwaapi/gwa-service.ts +++ b/src/services/gwaapi/gwa-service.ts @@ -1,6 +1,7 @@ import { checkStatus } from '../checkStatus'; import fetch from 'node-fetch'; import { logger } from '../../logger'; +import { de } from 'date-fns/locale'; export class GWAService { private gwaUrl: string; @@ -42,9 +43,15 @@ export class GWAService { const url = `${this.gwaUrl}/v2/namespaces/${ns}/gateway/pattern-output`; logger.debug('[getGatewayConfigUsingPattern] ns=%s', ns); + const deleteQualifier = deleteFlag ? payload.ns_qualifier : ''; + return await fetch(url, { method: 'put', - body: JSON.stringify({ delete: deleteFlag, document: payload }), + body: JSON.stringify({ + delete: deleteFlag, + deleteQualifier, + document: payload, + }), headers: { 'Content-Type': 'application/json', }, From 7213e227f96603e54dcfc9b4936837ef683bd887 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 09:56:25 -0800 Subject: [PATCH 100/109] upd sdx controller --- src/controllers/v3/SDXController.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts index 4f63bd5e5..c69354c5e 100644 --- a/src/controllers/v3/SDXController.ts +++ b/src/controllers/v3/SDXController.ts @@ -11,6 +11,7 @@ import { Tags, FormField, UploadedFile, + ValidateError, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; @@ -25,6 +26,7 @@ import { GatewayRoute } from './types'; import { PublishResult } from './types-extra'; import { CatalogEntry, GetCatalog } from '../../services/sdx/sdx-catalog'; import { GetConfigUsingPattern } from '../../services/sdx/gateway-patterns'; +import { assertEqual } from '../ioc/assert'; interface GatewayPatternConfigRequest { pattern: string; @@ -51,15 +53,18 @@ export class SDXController extends Controller { @Request() request: any ): Promise { const ctx = this.keystone.createContext(request); - return await GetConfigUsingPattern(ctx, body); + try { + return await GetConfigUsingPattern(ctx, body); + } catch (error) { + assertEqual(true, true, 'input', error.message); + } } - @Get() + @Get('/catalog') @OperationId('get-catalog') @Security('jwt', []) public async getCatalog(@Request() request: any): Promise { const ctx = this.keystone.createContext(request); - return await GetCatalog(ctx); } } From a36b7b65d1d60766207a45a3449ead42c1426666 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 12:15:02 -0800 Subject: [PATCH 101/109] adj patterns --- src/services/sdx/gateway-patterns.ts | 6 +++--- src/services/sdx/sdx-catalog.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index 121e742cb..30913d7a4 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -138,10 +138,10 @@ async function evalConsumerPattern( consumer_uri: consumer.locator, gateway: consumer.gateway.name, ns_qualifier: `AP-C-REQ-${reqId}`, - route_host: consumer.edgeServer.host, + route_host: consumer.edgeServer.internal_endpoint, route_path: `/${provider.locator}`, service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, - upstream_uri: `https://${provider.edgeServer.host}`, + upstream_uri: `https://${provider.edgeServer.endpoint}`, } ); return result; @@ -166,7 +166,7 @@ async function evalProviderPattern( gateway: provider.gateway.name, mtls_allow_list: `"${provider.edgeServer.dn}"`, ns_qualifier: `AP-P-REQ-${reqId}`, - route_host: provider.edgeServer.host, + route_host: provider.edgeServer.endpoint, route_path: `/${provider.locator}`, service_name: `AP-P-REQ-${reqId}-${provider.product.name}`, upstream_uri: upstreamUri, diff --git a/src/services/sdx/sdx-catalog.ts b/src/services/sdx/sdx-catalog.ts index 571546445..45c6131d0 100644 --- a/src/services/sdx/sdx-catalog.ts +++ b/src/services/sdx/sdx-catalog.ts @@ -30,6 +30,8 @@ export interface CatalogEntry { edgeServer: { host: string; dn: string; + endpoint: string; + internal_endpoint: string; }; hasSpec: boolean; } From d0f0e0433205ab43089a18a3141d2bcddf544d80 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 12:26:10 -0800 Subject: [PATCH 102/109] adj error --- src/controllers/v3/SDXController.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/controllers/v3/SDXController.ts b/src/controllers/v3/SDXController.ts index c69354c5e..cb3f99777 100644 --- a/src/controllers/v3/SDXController.ts +++ b/src/controllers/v3/SDXController.ts @@ -53,11 +53,7 @@ export class SDXController extends Controller { @Request() request: any ): Promise { const ctx = this.keystone.createContext(request); - try { - return await GetConfigUsingPattern(ctx, body); - } catch (error) { - assertEqual(true, true, 'input', error.message); - } + return await GetConfigUsingPattern(ctx, body); } @Get('/catalog') From ffe604004f4cb830fdd7beebf193cbf46cb4f3e3 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 13:10:25 -0800 Subject: [PATCH 103/109] upd missing edge server details --- src/services/sdx/sdx-catalog.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/sdx/sdx-catalog.ts b/src/services/sdx/sdx-catalog.ts index 45c6131d0..299cfd1f8 100644 --- a/src/services/sdx/sdx-catalog.ts +++ b/src/services/sdx/sdx-catalog.ts @@ -110,6 +110,8 @@ export async function GetCatalog(ctx: any): Promise { env.edgeServer = { host: edgeServer.host, dn: edgeServer.dn, + endpoint: edgeServer.endpoint, + internal_endpoint: edgeServer.internal_endpoint, }; }); await Promise.all(promises); From 9504cb2cb89917683eb8942ef88d7a1256711817 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 15:17:05 -0800 Subject: [PATCH 104/109] adj gateway pattern --- src/services/sdx/gateway-patterns.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index 30913d7a4..8348a6ad5 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -139,6 +139,7 @@ async function evalConsumerPattern( gateway: consumer.gateway.name, ns_qualifier: `AP-C-REQ-${reqId}`, route_host: consumer.edgeServer.internal_endpoint, + provider_endpoint: `${provider.edgeServer.endpoint}`, route_path: `/${provider.locator}`, service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, upstream_uri: `https://${provider.edgeServer.endpoint}`, From 59d74ff9eb17566d2ea886973561c5d1bcc3ce64 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 15:40:31 -0800 Subject: [PATCH 105/109] adj pattern --- src/services/sdx/gateway-patterns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index 8348a6ad5..76b0e3173 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -139,7 +139,7 @@ async function evalConsumerPattern( gateway: consumer.gateway.name, ns_qualifier: `AP-C-REQ-${reqId}`, route_host: consumer.edgeServer.internal_endpoint, - provider_endpoint: `${provider.edgeServer.endpoint}`, + provider_endpoint: `${provider.edgeServer.host}`, route_path: `/${provider.locator}`, service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, upstream_uri: `https://${provider.edgeServer.endpoint}`, @@ -167,7 +167,7 @@ async function evalProviderPattern( gateway: provider.gateway.name, mtls_allow_list: `"${provider.edgeServer.dn}"`, ns_qualifier: `AP-P-REQ-${reqId}`, - route_host: provider.edgeServer.endpoint, + route_host: provider.edgeServer.host, route_path: `/${provider.locator}`, service_name: `AP-P-REQ-${reqId}-${provider.product.name}`, upstream_uri: upstreamUri, From 875eff2b1b06a66083aa746670392c235704fda6 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 20:17:04 -0800 Subject: [PATCH 106/109] add edge_kid --- src/services/sdx/gateway-patterns.ts | 1 + src/services/sdx/sdx-catalog.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index 76b0e3173..fac23d22e 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -171,6 +171,7 @@ async function evalProviderPattern( route_path: `/${provider.locator}`, service_name: `AP-P-REQ-${reqId}-${provider.product.name}`, upstream_uri: upstreamUri, + edge_kid: `urn:ca:bc:sdx:edge:${provider.edgeServer.id}:0`, } ); return result; diff --git a/src/services/sdx/sdx-catalog.ts b/src/services/sdx/sdx-catalog.ts index 299cfd1f8..4e4a5cdfd 100644 --- a/src/services/sdx/sdx-catalog.ts +++ b/src/services/sdx/sdx-catalog.ts @@ -7,6 +7,7 @@ import { getGwaProductEnvironment } from '../workflow'; import { dynamicallySetEnvironmentDetails } from '../keystone'; import { LookupMemberOrganization } from './member-id'; import { LookupEdgeServer } from './edge-servers'; +import { id } from 'date-fns/locale'; export interface CatalogEntry { id: string; @@ -28,6 +29,7 @@ export interface CatalogEntry { }; }; edgeServer: { + id: string; host: string; dn: string; endpoint: string; @@ -108,6 +110,7 @@ export async function GetCatalog(ctx: any): Promise { env.gateway.permissions.domains[0] ); env.edgeServer = { + id: edgeServer.id, host: edgeServer.host, dn: edgeServer.dn, endpoint: edgeServer.endpoint, From 7b638e4e83494abd59aca0c1ec0d24ddac9e9124 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 4 Dec 2025 20:17:26 -0800 Subject: [PATCH 107/109] add edge_kid --- src/services/sdx/gateway-patterns.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index fac23d22e..35d515aba 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -143,6 +143,7 @@ async function evalConsumerPattern( route_path: `/${provider.locator}`, service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, upstream_uri: `https://${provider.edgeServer.endpoint}`, + edge_kid: `urn:ca:bc:sdx:edge:${consumer.edgeServer.id}:0`, } ); return result; From b995b8babb1b754f7b1f64038a117cbbe4fdc9ba Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 5 Dec 2025 17:01:43 -0800 Subject: [PATCH 108/109] add jwks endpoint to org --- src/services/sdx/gateway-patterns.ts | 2 ++ src/services/sdx/sdx-catalog.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/services/sdx/gateway-patterns.ts b/src/services/sdx/gateway-patterns.ts index 35d515aba..e9f09d357 100644 --- a/src/services/sdx/gateway-patterns.ts +++ b/src/services/sdx/gateway-patterns.ts @@ -144,6 +144,7 @@ async function evalConsumerPattern( service_name: `AP-C-REQ-${reqId}-${provider.product.name}`, upstream_uri: `https://${provider.edgeServer.endpoint}`, edge_kid: `urn:ca:bc:sdx:edge:${consumer.edgeServer.id}:0`, + trust_jwks_endpoint: 'http://localhost:8000/jwks', // TODO: update to route for real endpoint } ); return result; @@ -173,6 +174,7 @@ async function evalProviderPattern( service_name: `AP-P-REQ-${reqId}-${provider.product.name}`, upstream_uri: upstreamUri, edge_kid: `urn:ca:bc:sdx:edge:${provider.edgeServer.id}:0`, + trust_jwks_endpoint: 'http://localhost:8000/jwks', // TODO: update to route for real endpoint } ); return result; diff --git a/src/services/sdx/sdx-catalog.ts b/src/services/sdx/sdx-catalog.ts index 4e4a5cdfd..6e27468a4 100644 --- a/src/services/sdx/sdx-catalog.ts +++ b/src/services/sdx/sdx-catalog.ts @@ -20,6 +20,7 @@ export interface CatalogEntry { organization: { name: string; orgUnit?: string; + trustJwksEndpoint?: string; }; gateway: { name: string; @@ -99,6 +100,7 @@ export async function GetCatalog(ctx: any): Promise { ] .join('.') .toUpperCase(); + env.organization.trustJwksEndpoint = member.trust_jwks_endpoint; const nsAttributes = await getNamespaceAttributes(svc, env.gateway.name); env.gateway.permissions = { From fc56ef96844b8b349fd0a213edc798e2c18ff758 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 16 Dec 2025 22:08:41 -0800 Subject: [PATCH 109/109] integrated test for product apispec --- .../integrated/keystonejs/product-apispec.ts | 111 ++++++++++++++++-- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/src/test/integrated/keystonejs/product-apispec.ts b/src/test/integrated/keystonejs/product-apispec.ts index 856012a45..a0d919589 100644 --- a/src/test/integrated/keystonejs/product-apispec.ts +++ b/src/test/integrated/keystonejs/product-apispec.ts @@ -18,14 +18,35 @@ import { parseBlobString, } from '../../../batch/feed-worker'; import { o } from '../util'; -import { lookupServiceAccessesByEnvironment } from '../../../services/keystone'; +import { dynamicallySetEnvironmentDetails, lookupServiceAccessesByEnvironment } from '../../../services/keystone'; import { getActivity, recordActivity, recordActivityWithBlob, } from '../../../services/keystone/activity'; import { id } from 'date-fns/locale'; -import {UpdateAPISpec, GetAPISpecsByOrg} from '../../../services/workflow/api-specs'; +import { + UpdateAPISpec, + GetAPISpecsByOrg, +} from '../../../services/workflow/api-specs'; +import { Environment } from '../../../services/keystone/types'; +import { gql } from 'graphql-request'; +import YAML from 'yaml'; +import { OrgNamespace } from '../../../services/org-groups/types'; +import { getGwaProductEnvironment } from '../../../services/workflow'; +import { NamespaceService } from '../../../services/org-groups'; + +async function getNamespaceAttributes( + ctx: any, + ns: string +): Promise { + const prodEnv = await getGwaProductEnvironment(ctx, false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + return await svc.getNamespaceOrganizationDetails(ns); +} (async () => { const keystone = await InitKeystone(); @@ -52,19 +73,95 @@ import {UpdateAPISpec, GetAPISpecsByOrg} from '../../../services/workflow/api-sp }); if (true) { - const result = await GetAPISpecsByOrg(ctx, 'ministry-of-puppies-and-kittens'); + const list = gql` + query OrgProductCatalog { + allEnvironments { + appId + name + spec { + blob + } + credentialIssuer { + name + clientId + inheritFrom { + environmentDetails + } + } + product { + name + namespace + organization { + name + } + } + } + } + `; + + const result = await keystone.executeGraphQL({ + context: ctx, + query: list, + }); + o(result); + const envs = result.data.allEnvironments.filter( + (e: Environment) => e.product.organization != null + ); + + const output = envs.map((env: any) => { + if (env.credentialIssuer != null) { + const envDetails = JSON.parse(dynamicallySetEnvironmentDetails(env.credentialIssuer)); + const credEnv = envDetails.find((e: any) => e.environment === env.name); + o(env) + env.credentialIssuer = { + issuerUrl: credEnv?.issuerUrl, + clientId: credEnv?.clientId, + } + o(env) + } + }); + + // o(env.credentialIssuer?.environmentDetails) + // return + // const envs = JSON.parse(env.credentialIssuer?.environmentDetails); + + // const issuerEnv = envs?.find((e: any) => e.environment === env.name); + // o(issuerEnv); + // return { + // appId: env.appId, + // name: env.name, + // credentialIssuer: { + // issuer: issuerEnv.issuerUrl, + // clientId: issuerEnv.clientId, + // }, + // product: { + // name: env.product.name, + // namespace: env.product.namespace, + // organization: { + // name: env.product.organization.name, + // }, + // }, + // }; + // }); + // o(output); + } + + if (false) { + const result = await GetAPISpecsByOrg( + ctx, + 'ministry-of-puppies-and-kittens' + ); o(result); } if (false) { - const spec= 'https://bcgov.github.io/sdx-openapi/%3CService%3E.v1.yaml'; - const result = await UpdateAPISpec(ctx, spec, 'E7FEB796'); + const spec = 'https://bcgov.github.io/sdx-openapi/%3CService%3E.v1.yaml'; + const result = await UpdateAPISpec(ctx, spec, 'E7FEB796'); o(result); } if (false) { // upgrade - const variables = { id: '20', namespace: ns, @@ -131,7 +228,7 @@ patterns: }`, variables, }); - o(getSpec); + o(getSpec); console.log(getSpec.data.allEnvironments[0].spec.blob); }