Skip to content

Commit

Permalink
TechDebt: Add missing pipeline keycloak changes
Browse files Browse the repository at this point in the history
  • Loading branch information
NickPhura committed Jan 4, 2024
1 parent 0f6ba8b commit a03b7a6
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 175 deletions.
66 changes: 42 additions & 24 deletions .config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,37 +41,55 @@
},
"sso": {
"dev": {
"url": "https://dev.loginproxy.gov.bc.ca/auth",
"clientId": "bio-hub-browser-4230",
"host": "https://dev.loginproxy.gov.bc.ca/auth",
"realm": "standard",
"integrationId": "4230",
"adminHost": "https://loginproxy.gov.bc.ca/auth",
"adminUserName": "biohub-svc-4466",
"apiHost": "https://api.loginproxy.gov.bc.ca/api/v1",
"keycloakSecret": "keycloak-admin-password",
"keycloakSecretAdminPassword": "keycloak_admin_password"
"clientId": "bio-hub-browser-4230",
"keycloakSecret": "keycloak",
"serviceClient": {
"serviceClientName": "biohub-svc-4466",
"keycloakSecretServiceClientPasswordKey": "biohub_svc_client_password"
},
"cssApi": {
"cssApiTokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token",
"cssApiClientId": "service-account-team-1159-4197",
"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": "bio-hub-browser-4230",
"host": "https://test.loginproxy.gov.bc.ca/auth",
"realm": "standard",
"integrationId": "4230",
"adminHost": "https://loginproxy.gov.bc.ca/auth",
"adminUserName": "biohub-svc-4466",
"apiHost": "https://api.loginproxy.gov.bc.ca/api/v1",
"keycloakSecret": "keycloak-admin-password",
"keycloakSecretAdminPassword": "keycloak_admin_password"
"clientId": "bio-hub-browser-4230",
"keycloakSecret": "keycloak",
"serviceClient": {
"serviceClientName": "biohub-svc-4466",
"keycloakSecretServiceClientPasswordKey": "biohub_svc_client_password"
},
"cssApi": {
"cssApiTokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token",
"cssApiClientId": "service-account-team-1159-4197",
"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": "bio-hub-browser-4230",
"host": "https://loginproxy.gov.bc.ca/auth",
"realm": "standard",
"integrationId": "4230",
"adminHost": "https://loginproxy.gov.bc.ca/auth",
"adminUserName": "biohub-svc-4466",
"apiHost": "https://api.loginproxy.gov.bc.ca/api/v1",
"keycloakSecret": "keycloak-admin-password",
"keycloakSecretAdminPassword": "keycloak_admin_password"
"clientId": "bio-hub-browser-4230",
"keycloakSecret": "keycloak",
"serviceClient": {
"serviceClientName": "biohub-svc-4466",
"keycloakSecretServiceClientPasswordKey": "biohub_svc_client_password"
},
"cssApi": {
"cssApiTokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token",
"cssApiClientId": "service-account-team-1159-4197",
"cssApiHost": "https://api.loginproxy.gov.bc.ca/api/v1",
"keycloakSecretCssApiSecretKey": "css_api_client_secret",
"cssApiEnvironment": "prod"
}
}
}
}
20 changes: 13 additions & 7 deletions api/.pipeline/lib/api.deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@ const apiDeploy = async (settings) => {
TZ: phases[phase].tz,
DB_SERVICE_NAME: `${phases[phase].dbName}-postgresql${phases[phase].suffix}`,
// Keycloak
KEYCLOAK_ADMIN_USERNAME: phases[phase].sso.adminUserName,
KEYCLOAK_SECRET: phases[phase].sso.keycloakSecret,
KEYCLOAK_SECRET_ADMIN_PASSWORD: phases[phase].sso.keycloakSecretAdminPassword,
KEYCLOAK_HOST: phases[phase].sso.url,
KEYCLOAK_CLIENT_ID: phases[phase].sso.clientId,
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_CLIENT_ID: phases[phase].sso.clientId,
// Keycloak secret
KEYCLOAK_SECRET: phases[phase].sso.keycloakSecret,
// 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
Expand Down
56 changes: 41 additions & 15 deletions api/.pipeline/templates/api.dc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,49 @@ parameters:
description: Application timezone
required: false
value: 'America/Vancouver'
# 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
# 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
- name: API_PORT_DEFAULT
value: '6100'
- name: API_PORT_DEFAULT_NAME
description: Api default port name
value: '6100-tcp'
# Object Store (S3)
- name: OBJECT_STORE_SECRETS
description: Secrets used to read and write to the S3 storage
value: 'biohubbc-object-store'
Expand Down Expand Up @@ -203,23 +217,35 @@ objects:
name: ${DB_SERVICE_NAME}
- name: DB_PORT
value: '5432'
# 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}
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}
- name: CHANGE_VERSION
value: ${CHANGE_ID}
- name: NODE_ENV
Expand Down
110 changes: 39 additions & 71 deletions api/src/services/keycloak-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,33 @@ chai.use(sinonChai);

describe('KeycloakService', () => {
beforeEach(() => {
process.env.KEYCLOAK_HOST = 'test.host';
process.env.KEYCLOAK_REALM = 'test-realm';
process.env.KEYCLOAK_API_HOST = 'api-host';
process.env.KEYCLOAK_ADMIN_USERNAME = 'admin';
process.env.KEYCLOAK_ADMIN_PASSWORD = 'password';
process.env.KEYCLOAK_INTEGRATION_ID = '1234';
process.env.KEYCLOAK_ENVIRONMENT = 'test-env';
process.env.KEYCLOAK_API_TOKEN_URL = 'https://host.com/auth/token';
process.env.KEYCLOAK_API_CLIENT_ID = 'client-456';
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_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' } };

const axiosStub = sinon.stub(axios, 'post').resolves(mockAxiosResponse);

const keycloakService = new KeycloakService();

const response = await keycloakService.getKeycloakToken();
const response = await keycloakService.getKeycloakCssApiToken();

expect(response).to.eql('token');

expect(axiosStub).to.have.been.calledWith(
`${'test.host'}/realms/${'test-realm'}/protocol/openid-connect/token`,
`${'grant_type=client_credentials'}&${'client_id=admin'}&${'client_secret=password'}`,
'https://host.com/auth/token',
'grant_type=client_credentials&client_id=client-456&client_secret=secret',
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
Expand All @@ -49,7 +48,7 @@ describe('KeycloakService', () => {
const keycloakService = new KeycloakService();

try {
await keycloakService.getKeycloakToken();
await keycloakService.getKeycloakCssApiToken();

expect.fail();
} catch (error) {
Expand All @@ -59,13 +58,13 @@ describe('KeycloakService', () => {
});
});

describe('getUserByUsername', async () => {
it('authenticates with keycloak and returns an access token', async () => {
sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token');
describe('findIDIRUsers', async () => {
it('finds matching idir users', async () => {
sinon.stub(KeycloakService.prototype, 'getKeycloakCssApiToken').resolves('token');

const mockAxiosResponse = {
data: {
users: [
data: [
{
username: 'username',
email: 'email',
Expand All @@ -78,93 +77,62 @@ describe('KeycloakService', () => {
displayName: ['string4']
}
}
],
roles: []
]
}
};

const axiosStub = sinon.stub(axios, 'get').resolves(mockAxiosResponse);

const keycloakService = new KeycloakService();

const response = await keycloakService.getUserByUsername('test@idir');

expect(response).to.eql({
username: 'username',
email: 'email',
firstName: 'firstName',
lastName: 'lastName',
attributes: {
idir_user_guid: ['string1'],
idir_userid: ['string2'],
idir_guid: ['string3'],
displayName: ['string4']
}
});
const response = await keycloakService.findIDIRUsers({ guid: '123456789' });

expect(axiosStub).to.have.been.calledWith(
`${'api-host'}/integrations/${'1234'}/test-env/user-role-mappings?${'username=test%40idir'}`,
expect(response).to.eql([
{
headers: { authorization: 'Bearer token' }
username: 'username',
email: 'email',
firstName: 'firstName',
lastName: 'lastName',
attributes: {
idir_user_guid: ['string1'],
idir_userid: ['string2'],
idir_guid: ['string3'],
displayName: ['string4']
}
}
);
});

it('throws an error if no users are found', async () => {
sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token');

sinon.stub(axios, 'get').resolves({ data: { users: [], roles: [] } });

const keycloakService = new KeycloakService();

try {
await keycloakService.getUserByUsername('test@idir');
]);

expect.fail();
} catch (error) {
expect((error as ApiGeneralError).message).to.equal('Failed to get user info from keycloak');
expect((error as ApiGeneralError).errors).to.eql(['Found no matching keycloak users']);
}
expect(axiosStub).to.have.been.calledWith('https://api.host.com/auth/dev/idir/users?guid=123456789', {
headers: { authorization: 'Bearer token' }
});
});

it('throws an error if more than 1 user is found', async () => {
sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token');
it('throws an error if no data is returned', async () => {
sinon.stub(KeycloakService.prototype, 'getKeycloakCssApiToken').resolves('token');

sinon.stub(axios, 'get').resolves({
data: {
users: [
{
username: 'user1'
},
{
username: 'user2'
}
],
roles: []
}
});
sinon.stub(axios, 'get').resolves({ data: null });

const keycloakService = new KeycloakService();

try {
await keycloakService.getUserByUsername('test@idir');
await keycloakService.findIDIRUsers({ guid: '123456789' });

expect.fail();
} catch (error) {
expect((error as ApiGeneralError).message).to.equal('Failed to get user info from keycloak');
expect((error as ApiGeneralError).errors).to.eql(['Found too many matching keycloak users']);
expect((error as ApiGeneralError).errors).to.eql(['Found no matching keycloak idir users']);
}
});

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'));

const keycloakService = new KeycloakService();

try {
await keycloakService.getUserByUsername('test@idir');
await keycloakService.findIDIRUsers({ guid: '123456789' });

expect.fail();
} catch (error) {
Expand Down
Loading

0 comments on commit a03b7a6

Please sign in to comment.