diff --git a/.config/config.json b/.config/config.json index bdfdacf73a..30ddbc6891 100644 --- a/.config/config.json +++ b/.config/config.json @@ -34,37 +34,55 @@ }, "sso": { "dev": { - "url": "https://dev.loginproxy.gov.bc.ca/auth", - "clientId": "sims-4461", + "host": "https://dev.loginproxy.gov.bc.ca/auth", "realm": "standard", - "integrationId": "4461", - "adminHost": "https://loginproxy.gov.bc.ca/auth", - "adminUserName": "sims-svc-4464", - "apiHost": "https://api.loginproxy.gov.bc.ca/api/v1", - "keycloakSecret": "keycloak-admin-password", - "keycloakSecretAdminPassword": "keycloak_admin_password" + "clientId": "sims-4461", + "keycloakSecret": "keycloak", + "serviceClient": { + "serviceClientName": "sims-svc-4464", + "keycloakSecretServiceClientPasswordKey": "sims_svc_client_password" + }, + "cssApi": { + "cssApiTokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token", + "cssApiClientId": "service-account-team-1190-4229", + "cssApiHost": "https://api.loginproxy.gov.bc.ca/api/v1", + "keycloakSecretCssApiSecretKey": "css_api_client_secret", + "cssApiEnvironment": "dev" + } }, "test": { - "url": "https://test.loginproxy.gov.bc.ca/auth", - "clientId": "sims-4461", + "host": "https://test.loginproxy.gov.bc.ca/auth", "realm": "standard", - "integrationId": "4461", - "adminHost": "https://loginproxy.gov.bc.ca/auth", - "adminUserName": "sims-svc-4464", - "apiHost": "https://api.loginproxy.gov.bc.ca/api/v1", - "keycloakSecret": "keycloak-admin-password", - "keycloakSecretAdminPassword": "keycloak_admin_password" + "clientId": "sims-4461", + "keycloakSecret": "keycloak", + "serviceClient": { + "serviceClientName": "sims-svc-4464", + "keycloakSecretServiceClientPasswordKey": "sims_svc_client_password" + }, + "cssApi": { + "cssApiTokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token", + "cssApiClientId": "service-account-team-1190-4229", + "cssApiHost": "https://api.loginproxy.gov.bc.ca/api/v1", + "keycloakSecretCssApiSecretKey": "css_api_client_secret", + "cssApiEnvironment": "test" + } }, "prod": { - "url": "https://loginproxy.gov.bc.ca/auth", - "clientId": "sims-4461", + "host": "https://loginproxy.gov.bc.ca/auth", "realm": "standard", - "integrationId": "4461", - "adminHost": "https://loginproxy.gov.bc.ca/auth", - "adminUserName": "sims-svc-4464", - "apiHost": "https://api.loginproxy.gov.bc.ca/api/v1", - "keycloakSecret": "keycloak-admin-password", - "keycloakSecretAdminPassword": "keycloak_admin_password" + "clientId": "sims-4461", + "keycloakSecret": "keycloak", + "serviceClient": { + "serviceClientName": "sims-svc-4464", + "keycloakSecretServiceClientPasswordKey": "sims_svc_client_password" + }, + "cssApi": { + "cssApiTokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token", + "cssApiClientId": "service-account-team-1190-4229", + "cssApiHost": "https://api.loginproxy.gov.bc.ca/api/v1", + "keycloakSecretCssApiSecretKey": "css_api_client_secret", + "cssApiEnvironment": "prod" + } } } } diff --git a/api/.pipeline/lib/api.deploy.js b/api/.pipeline/lib/api.deploy.js index d9781f7306..30ddefb8bb 100644 --- a/api/.pipeline/lib/api.deploy.js +++ b/api/.pipeline/lib/api.deploy.js @@ -29,32 +29,43 @@ const apiDeploy = async (settings) => { SUFFIX: phases[phase].suffix, VERSION: phases[phase].tag, HOST: phases[phase].host, - CHANGE_ID: phases.build.changeId || changeId, APP_HOST: phases[phase].appHost, + CHANGE_ID: phases.build.changeId || changeId, + NODE_ENV: phases[phase].env, + // BioHub Platform (aka: Backbone) BACKBONE_API_HOST: phases[phase].backboneApiHost, BACKBONE_INTAKE_PATH: phases[phase].backboneIntakePath, BACKBONE_ARTIFACT_INTAKE_PATH: phases[phase].backboneArtifactIntakePath, BACKBONE_INTAKE_ENABLED: phases[phase].backboneIntakeEnabled, + // BCTW / Critterbase BCTW_API_HOST: phases[phase].bctwApiHost, CB_API_HOST: phases[phase].critterbaseApiHost, - NODE_ENV: phases[phase].env || 'dev', + // Elastic Search ELASTICSEARCH_URL: phases[phase].elasticsearchURL, ELASTICSEARCH_TAXONOMY_INDEX: phases[phase].elasticsearchTaxonomyIndex, + // S3 S3_KEY_PREFIX: phases[phase].s3KeyPrefix, + // Database TZ: phases[phase].tz, - KEYCLOAK_ADMIN_USERNAME: phases[phase].sso.adminUserName, - KEYCLOAK_SECRET: phases[phase].sso.keycloakSecret, - KEYCLOAK_SECRET_ADMIN_PASSWORD: phases[phase].sso.keycloakSecretAdminPassword, DB_SERVICE_NAME: `${phases[phase].dbName}-postgresql${phases[phase].suffix}`, - KEYCLOAK_HOST: phases[phase].sso.url, - KEYCLOAK_CLIENT_ID: phases[phase].sso.clientId, + // Keycloak + KEYCLOAK_HOST: phases[phase].sso.host, KEYCLOAK_REALM: phases[phase].sso.realm, - KEYCLOAK_INTEGRATION_ID: phases[phase].sso.integrationId, - KEYCLOAK_API_HOST: phases[phase].sso.apiHost, - KEYCLOAK_ADMIN_USERNAME: phases[phase].sso.adminUserName, + KEYCLOAK_CLIENT_ID: phases[phase].sso.clientId, + // Keycloak secret KEYCLOAK_SECRET: phases[phase].sso.keycloakSecret, - KEYCLOAK_SECRET_ADMIN_PASSWORD: phases[phase].sso.keycloakSecretAdminPassword, + // Keycloak Service Client + KEYCLOAK_ADMIN_USERNAME: phases[phase].sso.serviceClient.serviceClientName, + KEYCLOAK_SECRET_ADMIN_PASSWORD_KEY: phases[phase].sso.serviceClient.keycloakSecretServiceClientPasswordKey, + // Keycloak CSS API + KEYCLOAK_API_TOKEN_URL: phases[phase].sso.cssApi.cssApiTokenUrl, + KEYCLOAK_API_CLIENT_ID: phases[phase].sso.cssApi.cssApiClientId, + KEYCLOAK_API_CLIENT_SECRET_KEY: phases[phase].sso.cssApi.keycloakSecretCssApiSecretKey, + KEYCLOAK_API_HOST: phases[phase].sso.cssApi.cssApiHost, + KEYCLOAK_API_ENVIRONMENT: phases[phase].sso.cssApi.cssApiEnvironment, + // Log Level LOG_LEVEL: phases[phase].logLevel || 'info', + // Openshift Resources CPU_REQUEST: phases[phase].cpuRequest, CPU_LIMIT: phases[phase].cpuLimit, MEMORY_REQUEST: phases[phase].memoryRequest, diff --git a/api/.pipeline/templates/api.dc.yaml b/api/.pipeline/templates/api.dc.yaml index 1b821fcd13..c72de21637 100644 --- a/api/.pipeline/templates/api.dc.yaml +++ b/api/.pipeline/templates/api.dc.yaml @@ -5,12 +5,6 @@ metadata: labels: build: biohubbc-api parameters: - - name: ENABLE_FILE_VIRUS_SCAN - value: 'true' - - name: CLAMAV_HOST - value: 'clamav' - - name: CLAMAV_PORT - value: '3310' - name: NAME value: biohubbc-api - name: SUFFIX @@ -24,15 +18,30 @@ parameters: - name: APP_HOST description: APP host for application frontend value: '' - - name: BACKBONE_API_HOST + - name: CHANGE_ID + description: Change id of the project. This will help to pull image stream required: true - description: API host for BioHub Platform Backbone. Example "https://platform.com". - - name: CB_API_HOST + value: '0' + - name: NODE_ENV + description: Application Environment type variable required: true - description: API host for the Critterbase service, SIMS API will hit this to retrieve critter metadata. Example "https://critterbase.com". - - name: BCTW_API_HOST + value: 'dev' + - name: API_PORT_DEFAULT + value: '6100' + - name: API_PORT_DEFAULT_NAME + description: Api default port name + value: '6100-tcp' + # Clamav + - name: ENABLE_FILE_VIRUS_SCAN + value: 'true' + - name: CLAMAV_HOST + value: 'clamav' + - name: CLAMAV_PORT + value: '3310' + # BioHub Platform (aka: Backbone) + - name: BACKBONE_API_HOST required: true - description: API host for the BC Telemetry Warehouse service. SIMS API will hit this for device deployments and other telemetry operations. Example "https://bctw.com". + description: API host for BioHub Platform Backbone. Example "https://platform.com". - name: BACKBONE_INTAKE_PATH required: true description: API path for BioHub Platform Backbone DwCA submission intake endpoint. Example "/api/path/to/intake". @@ -42,17 +51,14 @@ parameters: - name: BACKBONE_INTAKE_ENABLED required: true description: Controls whether or not SIMS will submit DwCA datasets to the BioHub Platform Backbone. Set to "true" to enable it, will be disabled by default otherwise. - - name: CHANGE_ID - description: Change id of the project. This will help to pull image stream - required: true - value: '0' - - name: DB_SERVICE_NAME - description: 'Database service name associated with deployment' + # BCTW / Critterbase + - name: CB_API_HOST + description: API host for the Critterbase service, SIMS API will hit this to retrieve critter metadata. Example "https://critterbase.com". required: true - - name: NODE_ENV - description: Application Environment type variable + - name: BCTW_API_HOST + description: API host for the BC Telemetry Warehouse service. SIMS API will hit this for device deployments and other telemetry operations. Example "https://bctw.com". required: true - value: 'dev' + # Elastic Search - name: ELASTICSEARCH_URL description: Platform Elasticsearch URL required: true @@ -61,48 +67,63 @@ parameters: description: Platform Elasticsearch Taxonomy Index required: true value: 'taxonomy_3.0.0' - - name: S3_KEY_PREFIX - description: S3 key optional prefix - required: false - value: 'sims' + # Database - name: TZ description: Application timezone required: false value: 'America/Vancouver' + - name: DB_SERVICE_NAME + description: 'Database service name associated with deployment' + required: true + # Keycloak - name: KEYCLOAK_HOST description: Key clock login url required: true - name: KEYCLOAK_REALM description: Realm identifier or name required: true - - name: KEYCLOAK_INTEGRATION_ID - description: keycloak integration id - required: true - - name: KEYCLOAK_API_HOST - description: keycloak API host - required: true - name: KEYCLOAK_CLIENT_ID description: Client Id for application required: true - - name: KEYCLOAK_ADMIN_USERNAME - description: keycloak host admin username - required: true + # Keycloak secret - name: KEYCLOAK_SECRET description: The name of the keycloak secret required: true - - name: KEYCLOAK_SECRET_ADMIN_PASSWORD + # Keycloak Service Client + - name: KEYCLOAK_ADMIN_USERNAME + description: keycloak host admin username + required: true + - name: KEYCLOAK_SECRET_ADMIN_PASSWORD_KEY description: The key of the admin password in the keycloak secret required: true - - name: API_PORT_DEFAULT - value: '6100' - - name: API_PORT_DEFAULT_NAME - description: Api default port name - value: '6100-tcp' + # Keycloak CSS API + - name: KEYCLOAK_API_TOKEN_URL + description: The url to fetch a css api access token, which is needed to call the css rest api + required: true + - name: KEYCLOAK_API_CLIENT_ID + description: The css api client id + required: true + - name: KEYCLOAK_API_CLIENT_SECRET_KEY + description: The css api client secret + required: true + - name: KEYCLOAK_API_HOST + description: The url of the css rest api + required: true + - name: KEYCLOAK_API_ENVIRONMENT + description: The css api environment to query (dev, test, prod) + required: true + # Object Store (S3) - name: OBJECT_STORE_SECRETS description: Secrets used to read and write to the S3 storage value: 'biohubbc-object-store' + - name: S3_KEY_PREFIX + description: S3 key optional prefix + required: false + value: 'sims' + # Log level - name: LOG_LEVEL value: 'info' + # GCNotify - name: GCNOTIFY_API_SECRET description: Secret for gcnotify api key value: 'gcnotify-api-key' @@ -122,6 +143,7 @@ parameters: value: https://api.notification.canada.ca/v2/notifications/email - name: GCNOTIFY_SMS_URL value: https://api.notification.canada.ca/v2/notifications/sms + # Openshift Resources - name: CPU_REQUEST value: '100m' - name: CPU_LIMIT @@ -194,24 +216,36 @@ objects: value: ${API_PORT_DEFAULT} - name: APP_HOST value: ${APP_HOST} + - name: VERSION + value: ${VERSION} + - name: CHANGE_VERSION + value: ${CHANGE_ID} + - name: NODE_ENV + value: ${NODE_ENV} + # BioHub Platform (aka: Backbone) - name: BACKBONE_API_HOST value: ${BACKBONE_API_HOST} - - name: CB_API_HOST - value: ${CB_API_HOST} - - name: BCTW_API_HOST - value: ${BCTW_API_HOST} - name: BACKBONE_INTAKE_PATH value: ${BACKBONE_INTAKE_PATH} - name: BACKBONE_ARTIFACT_INTAKE_PATH value: ${BACKBONE_ARTIFACT_INTAKE_PATH} - name: BACKBONE_INTAKE_ENABLED value: ${BACKBONE_INTAKE_ENABLED} + # BCTW / Critterbase + - name: CB_API_HOST + value: ${CB_API_HOST} + - name: BCTW_API_HOST + value: ${BCTW_API_HOST} + # Clamav - name: ENABLE_FILE_VIRUS_SCAN value: ${ENABLE_FILE_VIRUS_SCAN} - name: CLAMAV_HOST value: ${CLAMAV_HOST} - name: CLAMAV_PORT value: ${CLAMAV_PORT} + # Database + - name: TZ + value: ${TZ} - name: DB_HOST value: ${DB_SERVICE_NAME} - name: DB_USER_API @@ -233,37 +267,43 @@ objects: value: '5432' - name: PROJECT_SEEDER_USER_IDENTIFIER value: ${PROJECT_SEEDER_USER_IDENTIFIER} + # Keycloak - name: KEYCLOAK_HOST value: ${KEYCLOAK_HOST} - - name: KEYCLOAK_API_HOST - value: ${KEYCLOAK_API_HOST} - name: KEYCLOAK_REALM value: ${KEYCLOAK_REALM} - name: KEYCLOAK_CLIENT_ID value: ${KEYCLOAK_CLIENT_ID} - - name: KEYCLOAK_INTEGRATION_ID - value: ${KEYCLOAK_INTEGRATION_ID} + # Keycloak Service Client - name: KEYCLOAK_ADMIN_USERNAME value: ${KEYCLOAK_ADMIN_USERNAME} - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: secretKeyRef: name: ${KEYCLOAK_SECRET} - key: ${KEYCLOAK_SECRET_ADMIN_PASSWORD} - - name: CHANGE_VERSION - value: ${CHANGE_ID} - - name: NODE_ENV - value: ${NODE_ENV} + key: ${KEYCLOAK_SECRET_ADMIN_PASSWORD_KEY} + # Keycloak CSS API + - name: KEYCLOAK_API_TOKEN_URL + value: ${KEYCLOAK_API_TOKEN_URL} + - name: KEYCLOAK_API_CLIENT_ID + value: ${KEYCLOAK_API_CLIENT_ID} + - name: KEYCLOAK_API_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: ${KEYCLOAK_SECRET} + key: ${KEYCLOAK_API_CLIENT_SECRET_KEY} + - name: KEYCLOAK_API_HOST + value: ${KEYCLOAK_API_HOST} + - name: KEYCLOAK_API_ENVIRONMENT + value: ${KEYCLOAK_API_ENVIRONMENT} + # Elastic Search - name: ELASTICSEARCH_URL value: ${ELASTICSEARCH_URL} - name: ELASTICSEARCH_TAXONOMY_INDEX value: ${ELASTICSEARCH_TAXONOMY_INDEX} - name: S3_KEY_PREFIX value: ${S3_KEY_PREFIX} - - name: TZ - value: ${TZ} - - name: VERSION - value: ${VERSION} + # Object Store (S3) - name: OBJECT_STORE_URL valueFrom: secretKeyRef: @@ -284,8 +324,10 @@ objects: secretKeyRef: key: object_store_bucket_name name: ${OBJECT_STORE_SECRETS} + # Log level - name: LOG_LEVEL value: ${LOG_LEVEL} + # GCNotify - name: GCNOTIFY_SECRET_API_KEY valueFrom: secretKeyRef: diff --git a/api/src/openapi/schemas/project.ts b/api/src/openapi/schemas/project.ts index d94dc4446d..31a521e5c7 100644 --- a/api/src/openapi/schemas/project.ts +++ b/api/src/openapi/schemas/project.ts @@ -28,7 +28,8 @@ export const projectCreatePostRequestObject = { }, end_date: { type: 'string', - description: 'ISO 8601 date string' + description: 'ISO 8601 date string', + nullable: true } } }, diff --git a/api/src/paths/gcnotify/send.test.ts b/api/src/paths/gcnotify/send.test.ts index 6089dc8d25..c16b5227e9 100644 --- a/api/src/paths/gcnotify/send.test.ts +++ b/api/src/paths/gcnotify/send.test.ts @@ -24,8 +24,8 @@ describe('gcnotify', () => { recipient: { emailAddress: 'test@email.com', phoneNumber: null, userId: null }, message: { header: 'Hello TEST,', - body1: 'This is a message from the Species Inventory Management System (((env))) ((url)).', - body2: 'Your request to become an ((request_type)) was received on ((request_date)).', + main_body1: 'This is a message from the Species Inventory Management System (((env))) ((url)).', + main_body2: 'Your request to become an ((request_type)) was received on ((request_date)).', footer: 'We will contact you after your request has been reviewed by a member of our team.' } } @@ -67,8 +67,8 @@ describe('gcnotify', () => { recipient: { emailAddress: null, phoneNumber: 2501231231, userId: null }, message: { header: 'Hello TEST,', - body1: 'This is a message from the Species Inventory Management System (((env))) ((url)).', - body2: 'Your request to become an ((request_type)) was received on ((request_date)).', + main_body1: 'This is a message from the Species Inventory Management System (((env))) ((url)).', + main_body2: 'Your request to become an ((request_type)) was received on ((request_date)).', footer: 'We will contact you after your request has been reviewed by a member of our team.' } }; diff --git a/api/src/paths/gcnotify/send.ts b/api/src/paths/gcnotify/send.ts index 556ebb75b5..31ef877c88 100644 --- a/api/src/paths/gcnotify/send.ts +++ b/api/src/paths/gcnotify/send.ts @@ -57,7 +57,7 @@ POST.apiDoc = { }, message: { type: 'object', - required: ['subject', 'header', 'body1', 'body2', 'footer'], + required: ['subject', 'header', 'main_body1', 'main_body2', 'footer'], properties: { subject: { type: 'string' @@ -65,10 +65,10 @@ POST.apiDoc = { header: { type: 'string' }, - body1: { + main_body1: { type: 'string' }, - body2: { + main_body2: { type: 'string' }, footer: { diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 7d945be372..424e13f02e 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -82,7 +82,8 @@ POST.apiDoc = { }, end_date: { type: 'string', - description: 'ISO 8601 date string' + description: 'ISO 8601 date string', + nullable: true }, survey_types: { type: 'array', diff --git a/api/src/services/keycloak-service.test.ts b/api/src/services/keycloak-service.test.ts index 916d478969..ea735ce889 100644 --- a/api/src/services/keycloak-service.test.ts +++ b/api/src/services/keycloak-service.test.ts @@ -14,14 +14,14 @@ describe('KeycloakService', () => { process.env.KEYCLOAK_API_CLIENT_SECRET = 'secret'; process.env.KEYCLOAK_API_HOST = 'https://api.host.com/auth'; process.env.KEYCLOAK_INTEGRATION_ID = '123'; - process.env.KEYCLOAK_ENVIRONMENT = 'dev'; + process.env.KEYCLOAK_API_ENVIRONMENT = 'dev'; }); afterEach(() => { sinon.restore(); }); - describe('getKeycloakToken', async () => { + describe('getKeycloakCssApiToken', async () => { it('authenticates with keycloak and returns an access token', async () => { const mockAxiosResponse = { data: { access_token: 'token' } }; @@ -29,7 +29,7 @@ describe('KeycloakService', () => { const keycloakService = new KeycloakService(); - const response = await keycloakService.getKeycloakToken(); + const response = await keycloakService.getKeycloakCssApiToken(); expect(response).to.eql('token'); @@ -48,7 +48,7 @@ describe('KeycloakService', () => { const keycloakService = new KeycloakService(); try { - await keycloakService.getKeycloakToken(); + await keycloakService.getKeycloakCssApiToken(); expect.fail(); } catch (error) { @@ -60,7 +60,7 @@ describe('KeycloakService', () => { describe('findIDIRUsers', async () => { it('finds matching idir users', async () => { - sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + sinon.stub(KeycloakService.prototype, 'getKeycloakCssApiToken').resolves('token'); const mockAxiosResponse = { data: { @@ -108,7 +108,7 @@ describe('KeycloakService', () => { }); it('throws an error if no data is returned', async () => { - sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + sinon.stub(KeycloakService.prototype, 'getKeycloakCssApiToken').resolves('token'); sinon.stub(axios, 'get').resolves({ data: null }); @@ -125,7 +125,7 @@ describe('KeycloakService', () => { }); it('catches and re-throws an error', async () => { - sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + sinon.stub(KeycloakService.prototype, 'getKeycloakCssApiToken').resolves('token'); sinon.stub(axios, 'get').rejects(new Error('a test error')); diff --git a/api/src/services/keycloak-service.ts b/api/src/services/keycloak-service.ts index 9e221d0250..99cd37f365 100644 --- a/api/src/services/keycloak-service.ts +++ b/api/src/services/keycloak-service.ts @@ -57,23 +57,24 @@ const defaultLog = getLogger('services/keycloak-service'); * @class KeycloakService */ export class KeycloakService { - // Used to authenticate with the SIMS Service Credentials keycloakHost: string; + + // Used to authenticate with the SIMS Service Client keycloakServiceClientId: string; keycloakServiceClientSecret: string; - // Used to authenticate with the CSS API using the SIMS API credentials + // Used to authenticate with the SIMS CSS API keycloakApiTokenUrl: string; keycloakApiClientId: string; keycloakApiClientSecret: string; // Used to query the CSS API keycloakApiHost: string; - keycloakIntegrationId: string; - keycloakEnvironment: string; + keycloakApiEnvironment: string; constructor() { this.keycloakHost = `${process.env.KEYCLOAK_HOST}`; + this.keycloakServiceClientId = `${process.env.KEYCLOAK_ADMIN_USERNAME}`; this.keycloakServiceClientSecret = `${process.env.KEYCLOAK_ADMIN_PASSWORD}`; @@ -82,8 +83,7 @@ export class KeycloakService { this.keycloakApiClientSecret = `${process.env.KEYCLOAK_API_CLIENT_SECRET}`; this.keycloakApiHost = `${process.env.KEYCLOAK_API_HOST}`; - this.keycloakIntegrationId = `${process.env.KEYCLOAK_INTEGRATION_ID}`; - this.keycloakEnvironment = `${process.env.KEYCLOAK_ENVIRONMENT}`; + this.keycloakApiEnvironment = `${process.env.KEYCLOAK_API_ENVIRONMENT}`; } /** @@ -121,7 +121,7 @@ export class KeycloakService { * @return {*} {Promise} * @memberof KeycloakService */ - async getKeycloakToken(): Promise { + async getKeycloakCssApiToken(): Promise { try { const { data } = await axios.post( this.keycloakApiTokenUrl, @@ -139,7 +139,7 @@ export class KeycloakService { return data.access_token as string; } catch (error) { - defaultLog.debug({ label: 'getKeycloakToken', message: 'error', error: error }); + defaultLog.debug({ label: 'getKeycloakCssApiToken', message: 'error', error: error }); throw new ApiGeneralError('Failed to authenticate with keycloak', [(error as Error).message]); } } @@ -157,11 +157,11 @@ export class KeycloakService { email?: string; guid?: string; }): Promise { - const token = await this.getKeycloakToken(); + const token = await this.getKeycloakCssApiToken(); try { const { data } = await axios.get( - `${this.keycloakApiHost}/${this.keycloakEnvironment}/idir/users?${qs.stringify({ guid: criteria.guid })}`, + `${this.keycloakApiHost}/${this.keycloakApiEnvironment}/idir/users?${qs.stringify({ guid: criteria.guid })}`, { headers: { authorization: `Bearer ${token}` } } diff --git a/api/src/services/platform-service.test.ts b/api/src/services/platform-service.test.ts index 9e734f2de8..5eb2d28a09 100644 --- a/api/src/services/platform-service.test.ts +++ b/api/src/services/platform-service.test.ts @@ -500,7 +500,9 @@ describe('PlatformService', () => { const mockDBConnection = getMockDBConnection(); const platformService = new PlatformService(mockDBConnection); - const getKeycloakTokenStub = sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + const getKeycloakServiceTokenStub = sinon + .stub(KeycloakService.prototype, 'getKeycloakServiceToken') + .resolves('token'); const axiosStub = sinon.stub(axios, 'post').resolves({ data: { queue_id: 1 } }); @@ -523,7 +525,7 @@ describe('PlatformService', () => { const response = await platformService._submitDwCADatasetToBioHub(dwcaDatasetMock); - expect(getKeycloakTokenStub).to.have.been.calledOnce; + expect(getKeycloakServiceTokenStub).to.have.been.calledOnce; expect(axiosStub).to.have.been.calledOnceWith(sinon.match.string, sinon.match.any, sinon.match.object); expect(response).to.eql({ queue_id: 1 }); }); @@ -1154,7 +1156,9 @@ describe('PlatformService', () => { const mockDBConnection = getMockDBConnection(); const platformService = new PlatformService(mockDBConnection); - const keycloakServiceStub = sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + const getKeycloakServiceTokenStub = sinon + .stub(KeycloakService.prototype, 'getKeycloakServiceToken') + .resolves('token'); const axiosStub = sinon.stub(axios, 'post').resolves({ data: { @@ -1183,7 +1187,7 @@ describe('PlatformService', () => { await platformService._submitArtifactToBioHub(testArtifact); - expect(keycloakServiceStub).to.have.been.calledOnce; + expect(getKeycloakServiceTokenStub).to.have.been.calledOnce; expect(axiosStub).to.have.been.calledOnceWith( 'http://backbone-host.dev/api/artifact/intake', @@ -1210,7 +1214,9 @@ describe('PlatformService', () => { const mockDBConnection = getMockDBConnection(); const platformService = new PlatformService(mockDBConnection); - const keycloakServiceStub = sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + const getKeycloakServiceTokenStub = sinon + .stub(KeycloakService.prototype, 'getKeycloakServiceToken') + .resolves('token'); const axiosStub = sinon.stub(axios, 'post').resolves({ data: { @@ -1220,7 +1226,7 @@ describe('PlatformService', () => { await platformService.deleteAttachmentFromBiohub('uuid'); - expect(keycloakServiceStub).to.have.been.calledOnce; + expect(getKeycloakServiceTokenStub).to.have.been.calledOnce; expect(axiosStub).to.have.been.calledOnceWith( 'http://backbone-host.dev/api/artifact/delete', { diff --git a/api/src/services/platform-service.ts b/api/src/services/platform-service.ts index fdf679953c..abb3f029db 100644 --- a/api/src/services/platform-service.ts +++ b/api/src/services/platform-service.ts @@ -399,7 +399,7 @@ export class PlatformService extends DBService { async _submitDwCADatasetToBioHub(dwcaDataset: IDwCADataset): Promise<{ queue_id: number }> { const keycloakService = new KeycloakService(); - const token = await keycloakService.getKeycloakToken(); + const token = await keycloakService.getKeycloakServiceToken(); const formData = new FormData(); @@ -791,7 +791,7 @@ export class PlatformService extends DBService { const keycloakService = new KeycloakService(); - const token = await keycloakService.getKeycloakToken(); + const token = await keycloakService.getKeycloakServiceToken(); const formData = new FormData(); @@ -832,7 +832,7 @@ export class PlatformService extends DBService { const keycloakService = new KeycloakService(); - const token = await keycloakService.getKeycloakToken(); + const token = await keycloakService.getKeycloakServiceToken(); const backboneArtifactIntakeUrl = new URL(getBackboneArtifactDeletePath(), getBackboneApiHost()).href; diff --git a/app/.pipeline/lib/app.deploy.js b/app/.pipeline/lib/app.deploy.js index 232a9ffe86..2bc4df9f75 100644 --- a/app/.pipeline/lib/app.deploy.js +++ b/app/.pipeline/lib/app.deploy.js @@ -28,9 +28,9 @@ const appDeploy = async (settings) => { REACT_APP_SITEMINDER_LOGOUT_URL: phases[phase].siteminderLogoutURL, REACT_APP_MAX_UPLOAD_NUM_FILES: phases[phase].maxUploadNumFiles, REACT_APP_MAX_UPLOAD_FILE_SIZE: phases[phase].maxUploadFileSize, - NODE_ENV: phases[phase].env || 'dev', - REACT_APP_NODE_ENV: phases[phase].env || 'dev', - REACT_APP_KEYCLOAK_HOST: phases[phase].sso.url, + NODE_ENV: phases[phase].env, + REACT_APP_NODE_ENV: phases[phase].env, + REACT_APP_KEYCLOAK_HOST: phases[phase].sso.host, REACT_APP_KEYCLOAK_REALM: phases[phase].sso.realm, REACT_APP_KEYCLOAK_CLIENT_ID: phases[phase].sso.clientId, CPU_REQUEST: phases[phase].cpuRequest, diff --git a/app/src/features/surveys/CreateSurveyPage.tsx b/app/src/features/surveys/CreateSurveyPage.tsx index ca20835aa2..95387582da 100644 --- a/app/src/features/surveys/CreateSurveyPage.tsx +++ b/app/src/features/surveys/CreateSurveyPage.tsx @@ -179,6 +179,8 @@ const CreateSurveyPage = () => { DATE_FORMAT.ShortDateFormat, `Survey end date cannot be after ${getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, DATE_LIMIT.max)}` ) + .nullable() + .optional() }) .concat(PurposeAndMethodologyYupSchema) .concat(ProprietaryDataYupSchema) diff --git a/database/.pipeline/lib/db.setup.deploy.js b/database/.pipeline/lib/db.setup.deploy.js index e59288377f..73c39806b2 100644 --- a/database/.pipeline/lib/db.setup.deploy.js +++ b/database/.pipeline/lib/db.setup.deploy.js @@ -66,7 +66,7 @@ const dbSetupDeploy = async (settings) => { SUFFIX: phases[phase].suffix, VERSION: phases[phase].tag, CHANGE_ID: changeId, - NODE_ENV: phases[phase].env || 'dev', + NODE_ENV: phases[phase].env, DB_SERVICE_NAME: dbName, DB_SCHEMA: 'biohub', DB_SCHEMA_DAPI_V1: 'biohub_dapi_v1', diff --git a/database/src/migrations/20231018105500_api_delete_survey_critter.ts b/database/src/migrations/20231018105500_api_delete_survey_critter.ts new file mode 100644 index 0000000000..8b9acc82ae --- /dev/null +++ b/database/src/migrations/20231018105500_api_delete_survey_critter.ts @@ -0,0 +1,126 @@ +import { Knex } from 'knex'; + +/** + * Makes the lead biologist properties on a survey nullable, since the app no longer supports + * entering or editing biologists; Updates the survey delete procedure to delete from survey_location + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + SET search_path = 'biohub'; + + ALTER TABLE survey + ALTER COLUMN lead_first_name DROP NOT NULL, + ALTER COLUMN lead_last_name DROP NOT NULL; + + COMMENT ON COLUMN survey.lead_first_name IS '(Deprecated) The first name of the person who is the lead for the survey.'; + COMMENT ON COLUMN survey.lead_last_name IS '(Deprecated) The last name of the person who is the lead for the survey.'; + + --- Update survey delete procedure + + CREATE OR REPLACE PROCEDURE api_delete_survey(p_survey_id integer) + LANGUAGE plpgsql + SECURITY DEFINER + AS $procedure$ + -- ******************************************************************* + -- Procedure: api_delete_survey + -- Purpose: deletes a survey and dependencies + -- + -- MODIFICATION HISTORY + -- Person Date Comments + -- ---------------- ----------- -------------------------------------- + -- shreyas.devalapurkar@quartech.com + -- 2021-06-18 initial release + -- charlie.garrettjones@quartech.com + -- 2021-06-21 added occurrence submission delete + -- charlie.garrettjones@quartech.com + -- 2021-09-21 added survey summary submission delete + -- kjartan.einarsson@quartech.com + -- 2022-08-28 added survey_vantage, survey_spatial_component, survey delete + -- charlie.garrettjones@quartech.com + -- 2022-09-07 changes to permit model + -- charlie.garrettjones@quartech.com + -- 2022-10-05 1.3.0 model changes + -- charlie.garrettjones@quartech.com + -- 2022-10-05 1.5.0 model changes, drop concept of occurrence deletion for published data + -- charlie.garrettjones@quartech.com + -- 2023-03-14 1.7.0 model changes + -- alfred.rosenthal@quartech.com + -- 2023-03-15 added missing publish tables to survey delete + -- curtis.upshall@quartech.com + -- 2023-04-28 change order of survey delete procedure + -- alfred.rosenthal@quartech.com + -- 2023-07-26 delete regions + -- curtis.upshall@quartech.com + -- 2023-08-24 delete partnerships + -- curtis.upshall@quartech.com + -- 2023-08-24 delete survey blocks and stratums and participation + -- curtis.upshall@quartech.com + -- 2023-09-25 delete survey observations + -- alfred.rosenthal@quartech.com + -- 2023-09-25 delete survey critters + -- ******************************************************************* + declare + + begin + with occurrence_submissions as (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id), submission_spatial_components as (select submission_spatial_component_id from submission_spatial_component + where occurrence_submission_id in (select occurrence_submission_id from occurrence_submissions)) + delete from spatial_transform_submission where submission_spatial_component_id in (select submission_spatial_component_id from submission_spatial_components); + delete from submission_spatial_component where occurrence_submission_id in (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id); + + with occurrence_submissions as (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id), submission_statuses as (select submission_status_id from submission_status + where occurrence_submission_id in (select occurrence_submission_id from occurrence_submissions)) + delete from submission_message where submission_status_id in (select submission_status_id from submission_statuses); + delete from submission_status where occurrence_submission_id in (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id); + + delete from occurrence_submission_publish where occurrence_submission_id in (select occurrence_submission_id from occurrence_submission where survey_id = p_survey_id); + + delete from occurrence_submission where survey_id = p_survey_id; + + delete from survey_summary_submission_publish where survey_summary_submission_id in (select survey_summary_submission_id from survey_summary_submission where survey_id = p_survey_id); + delete from survey_summary_submission_message where survey_summary_submission_id in (select survey_summary_submission_id from survey_summary_submission where survey_id = p_survey_id); + delete from survey_summary_submission where survey_id = p_survey_id; + delete from survey_proprietor where survey_id = p_survey_id; + delete from survey_attachment_publish where survey_attachment_id in (select survey_attachment_id from survey_attachment where survey_id = p_survey_id); + delete from survey_attachment where survey_id = p_survey_id; + delete from survey_report_author where survey_report_attachment_id in (select survey_report_attachment_id from survey_report_attachment where survey_id = p_survey_id); + delete from survey_report_publish where survey_report_attachment_id in (select survey_report_attachment_id from survey_report_attachment where survey_id = p_survey_id); + delete from survey_report_attachment where survey_id = p_survey_id; + delete from study_species where survey_id = p_survey_id; + delete from survey_funding_source where survey_id = p_survey_id; + delete from survey_vantage where survey_id = p_survey_id; + delete from survey_spatial_component where survey_id = p_survey_id; + delete from survey_metadata_publish where survey_id = p_survey_id; + delete from survey_region where survey_id = p_survey_id; + delete from survey_first_nation_partnership where survey_id = p_survey_id; + delete from survey_block where survey_id = p_survey_id; + delete from permit where survey_id = p_survey_id; + delete from survey_type where survey_id = p_survey_id; + delete from survey_first_nation_partnership where survey_id = p_survey_id; + delete from survey_stakeholder_partnership where survey_id = p_survey_id; + delete from survey_participation where survey_id = p_survey_id; + delete from survey_stratum where survey_id = p_survey_id; + delete from survey_block where survey_id = p_survey_id; + delete from survey_site_strategy where survey_id = p_survey_id; + delete from survey_observation where survey_id = p_survey_id; + delete from survey_location where survey_id = p_survey_id; + delete from deployment where critter_id IN (select critter_id from critter where survey_id = p_survey_id); + delete from critter where survey_id = p_survey_id; + + -- delete the survey + delete from survey where survey_id = p_survey_id; + + exception + when others THEN + raise; + end; + $procedure$; + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +} diff --git a/docker-compose.yml b/docker-compose.yml index cc2bcb52cc..3bf730c2b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,37 +41,50 @@ services: - ${API_PORT}:${API_PORT} environment: - NODE_ENV=${NODE_ENV} + - API_HOST=${API_HOST} + - API_PORT=${API_PORT} + # Elastic Search - ELASTICSEARCH_URL=${ELASTICSEARCH_URL} - ELASTICSEARCH_TAXONOMY_INDEX=${ELASTICSEARCH_TAXONOMY_INDEX} - - S3_KEY_PREFIX=${S3_KEY_PREFIX} + # Database - TZ=${API_TZ} - - API_HOST=${API_HOST} - - API_PORT=${API_PORT} - DB_HOST=${DB_HOST} - DB_USER_API=${DB_USER_API} - DB_USER_API_PASS=${DB_USER_API_PASS} - DB_PORT=5432 - DB_DATABASE=${DB_DATABASE} - DB_SCHEMA=${DB_SCHEMA} + # Seed - PROJECT_SEEDER_USER_IDENTIFIER=${PROJECT_SEEDER_USER_IDENTIFIER} + # Keycloak - KEYCLOAK_HOST=${KEYCLOAK_HOST} - KEYCLOAK_REALM=${KEYCLOAK_REALM} + # Keycloak Service client - KEYCLOAK_ADMIN_USERNAME=${KEYCLOAK_ADMIN_USERNAME} - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} - - KEYCLOAK_INTEGRATION_ID=${KEYCLOAK_INTEGRATION_ID} - - KEYCLOAK_ENVIRONMENT=${KEYCLOAK_ENVIRONMENT} + # Keycloak CSS API + - KEYCLOAK_API_TOKEN_URL=${KEYCLOAK_API_TOKEN_URL} + - KEYCLOAK_API_CLIENT_ID=${KEYCLOAK_API_CLIENT_ID} + - KEYCLOAK_API_CLIENT_SECRET=${KEYCLOAK_API_CLIENT_SECRET} - KEYCLOAK_API_HOST=${KEYCLOAK_API_HOST} + - KEYCLOAK_API_ENVIRONMENT=${KEYCLOAK_API_ENVIRONMENT} + # Object Store (S3) - OBJECT_STORE_URL=${OBJECT_STORE_URL} - OBJECT_STORE_ACCESS_KEY_ID=${OBJECT_STORE_ACCESS_KEY_ID} - OBJECT_STORE_SECRET_KEY_ID=${OBJECT_STORE_SECRET_KEY_ID} - OBJECT_STORE_BUCKET_NAME=${OBJECT_STORE_BUCKET_NAME} + - S3_KEY_PREFIX=${S3_KEY_PREFIX} + # File Upload - MAX_REQ_BODY_SIZE=${MAX_REQ_BODY_SIZE} - MAX_UPLOAD_NUM_FILES=${MAX_UPLOAD_NUM_FILES} - MAX_UPLOAD_FILE_SIZE=${MAX_UPLOAD_FILE_SIZE} + # Log level - LOG_LEVEL=${LOG_LEVEL} + # Clamav - CLAMAV_PORT=${CLAMAV_PORT} - CLAMAV_HOST=${CLAMAV_HOST} - ENABLE_FILE_VIRUS_SCAN=${ENABLE_FILE_VIRUS_SCAN} + # GCNotify - GCNOTIFY_SECRET_API_KEY=${GCNOTIFY_SECRET_API_KEY} - GCNOTIFY_ADMIN_EMAIL=${GCNOTIFY_ADMIN_EMAIL} - GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE=${GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE} @@ -80,10 +93,12 @@ services: - GCNOTIFY_EMAIL_URL=${GCNOTIFY_EMAIL_URL} - GCNOTIFY_SMS_URL=${GCNOTIFY_SMS_URL} - APP_HOST=${APP_HOST} + # BioHub Platform (aka: Backbone) - BACKBONE_API_HOST=${BACKBONE_API_HOST} - BACKBONE_INTAKE_PATH=${BACKBONE_INTAKE_PATH} - BACKBONE_ARTIFACT_INTAKE_PATH=${BACKBONE_ARTIFACT_INTAKE_PATH} - BACKBONE_INTAKE_ENABLED=${BACKBONE_INTAKE_ENABLED} + # BCTW / Critterbase - BCTW_API_HOST=${BCTW_API_HOST} - CB_API_HOST=${CB_API_HOST} volumes: @@ -95,7 +110,6 @@ services: - db - db_setup - # Build the clamav docker image clamav: image: mkodockx/docker-clamav:latest diff --git a/env_config/env.docker b/env_config/env.docker index 290285fb54..6de434529c 100644 --- a/env_config/env.docker +++ b/env_config/env.docker @@ -75,6 +75,8 @@ CB_API_HOST=https://moe-critterbase-api-dev.apps.silver.devops.gov.bc.ca/api # ------------------------------------------------------------------------------ # Postgres Database +# +# See `biohubbc-creds` secret in openshift # ------------------------------------------------------------------------------ POSTGRES_VERSION=12.5 POSTGIS_VERSION=3 @@ -92,40 +94,32 @@ DB_TZ=America/Vancouver # ------------------------------------------------------------------------------ # KeyCloak Configuration for Keycloak Common Hosted Single Sign-on (CSS) # CSS: https://bcgov.github.io/sso-requests +# +# See `keycloak` secret in openshift # ------------------------------------------------------------------------------ # The host URL used to authenticate with Keycloak KEYCLOAK_HOST=https://dev.loginproxy.gov.bc.ca/auth - # The Keycloak Realm used for authentication KEYCLOAK_REALM=standard - # The identifier for the SIMS Browser Login CSS resource KEYCLOAK_CLIENT_ID=sims-4461 -# The identifier for the SIMS Keycloak CSS integration -KEYCLOAK_INTEGRATION_ID=4461 - -# The targeted Keycloak environment (dev, test or prod) -KEYCLOAK_ENVIRONMENT=dev - # The identifier for the SIMS Service User CSS resource KEYCLOAK_ADMIN_USERNAME=sims-svc-4464 - # The secret key for the SIMS Service User CSS resource KEYCLOAK_ADMIN_PASSWORD= # The identifier for the Keycloak CSS API KEYCLOAK_API_CLIENT_ID=service-account-team-1190-4229 - # The secret key for the Keycloak CSS API KEYCLOAK_API_CLIENT_SECRET= - # The Keycloak API Token URL (only used to generate the Bearer token required to call the KEYCLOAK_API_HOST) KEYCLOAK_API_TOKEN_URL=https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token - # The Keycloak API host URL KEYCLOAK_API_HOST=https://api.loginproxy.gov.bc.ca/api/v1 +# The targeted Keycloak environment (dev, test or prod) +KEYCLOAK_API_ENVIRONMENT=dev # ------------------------------------------------------------------------------ # File Upload @@ -141,11 +135,13 @@ MAX_UPLOAD_FILE_SIZE=52428800 # ------------------------------------------------------------------------------ # Object Store (S3) +# +# See `biohubbc-object-store` secret in openshift # ------------------------------------------------------------------------------ OBJECT_STORE_URL=nrs.objectstore.gov.bc.ca -OBJECT_STORE_ACCESS_KEY_ID= +OBJECT_STORE_ACCESS_KEY_ID=nr-sims-dlv OBJECT_STORE_SECRET_KEY_ID= -OBJECT_STORE_BUCKET_NAME= +OBJECT_STORE_BUCKET_NAME=gblhvt S3_KEY_PREFIX=local/sims # ------------------------------------------------------------------------------ @@ -163,6 +159,8 @@ ENABLE_FILE_VIRUS_SCAN=false # ------------------------------------------------------------------------------ # GCNotify - Email and SMS api +# +# See `gcnotify-api-key` secret in openshift # ------------------------------------------------------------------------------ GCNOTIFY_SECRET_API_KEY= GCNOTIFY_ADMIN_EMAIL=simulate-delivered@notification.canada.ca