From 1c37f805ef5e6376182d41616f87532ffb8176cd Mon Sep 17 00:00:00 2001 From: Dan Schultz Date: Mon, 28 Oct 2024 10:03:13 -0400 Subject: [PATCH] Support organizational roles Users in our system can be associated with the three types of organization in the PDC (changemakers, funders, and data providers). These associations will allow them access to perform various actions in the context of those organizations. For instance, reading data, writing data, or managing other user associations. This list of abilities may change in future. We explored the concept of `user_roles` with foreign keys to different organization types (similar to the sources table) but decided to have three separate tables because they are slightly distinct concepts. For instance, there will probably be certain access types that only apply to certain types of organization in future. Another design decision was to have the permissions in terms of granted access type rather than higher level role. For instance, instead of roles like "administrator" and "editor" we have action oriented roles like "read" and "manage". This provides more granularity and is also more explicit about what a given role / access type actually allows. Issue #1250 Support associations between users and organizational entities --- docs/ENTITY_RELATIONSHIP_DIAGRAM.md | 3 + package.json | 3 +- src/__tests__/users.int.test.ts | 117 +++++++++++++++++- .../changemaker_role_to_json.sql | 14 +++ .../data_provider_role_to_json.sql | 14 +++ .../initialization/funder_role_to_json.sql | 14 +++ src/database/initialization/user_to_json.sql | 48 +++++++ .../0040-create-changemaker_roles.sql | 17 +++ .../migrations/0041-create-funder_roles.sql | 11 ++ .../0042-create-data_provider_roles.sql | 11 ++ .../createOrUpdateChangemakerRole.ts | 32 +++++ .../operations/changemakerRoles/index.ts | 1 + .../createOrUpdateDataProviderRole.ts | 32 +++++ .../operations/dataProviderRoles/index.ts | 1 + .../funderRoles/createOrUpdateFunderRole.ts | 32 +++++ src/database/operations/funderRoles/index.ts | 1 + src/database/operations/index.ts | 3 + .../changemakerRoles/insertOrUpdateOne.sql | 14 +++ .../dataProviderRoles/insertOrUpdateOne.sql | 19 +++ .../queries/funderRoles/insertOrUpdateOne.sql | 14 +++ src/openapi.json | 114 +++++++++++++++++ src/types/AccessType.ts | 7 ++ src/types/ChangemakerRole.ts | 36 ++++++ src/types/DataProviderRole.ts | 37 ++++++ src/types/FunderRole.ts | 37 ++++++ src/types/index.ts | 3 + 26 files changed, 632 insertions(+), 3 deletions(-) create mode 100644 src/database/initialization/changemaker_role_to_json.sql create mode 100644 src/database/initialization/data_provider_role_to_json.sql create mode 100644 src/database/initialization/funder_role_to_json.sql create mode 100644 src/database/migrations/0040-create-changemaker_roles.sql create mode 100644 src/database/migrations/0041-create-funder_roles.sql create mode 100644 src/database/migrations/0042-create-data_provider_roles.sql create mode 100644 src/database/operations/changemakerRoles/createOrUpdateChangemakerRole.ts create mode 100644 src/database/operations/changemakerRoles/index.ts create mode 100644 src/database/operations/dataProviderRoles/createOrUpdateDataProviderRole.ts create mode 100644 src/database/operations/dataProviderRoles/index.ts create mode 100644 src/database/operations/funderRoles/createOrUpdateFunderRole.ts create mode 100644 src/database/operations/funderRoles/index.ts create mode 100644 src/database/queries/changemakerRoles/insertOrUpdateOne.sql create mode 100644 src/database/queries/dataProviderRoles/insertOrUpdateOne.sql create mode 100644 src/database/queries/funderRoles/insertOrUpdateOne.sql create mode 100644 src/types/AccessType.ts create mode 100644 src/types/ChangemakerRole.ts create mode 100644 src/types/DataProviderRole.ts create mode 100644 src/types/FunderRole.ts diff --git a/docs/ENTITY_RELATIONSHIP_DIAGRAM.md b/docs/ENTITY_RELATIONSHIP_DIAGRAM.md index 0e25a99a..33d51ace 100644 --- a/docs/ENTITY_RELATIONSHIP_DIAGRAM.md +++ b/docs/ENTITY_RELATIONSHIP_DIAGRAM.md @@ -132,6 +132,9 @@ erDiagram Proposal }o--|| User : "is created by" ProposalVersion }o--|| User : "is created by" BulkUpload }o--|| User : "is created by" + User }o--o{ Changemaker : "is associated with" + User }o--o{ Funder : "is associated with" + User }o--o{ DataProvider : "is associated with" ``` ## Narrative diff --git a/package.json b/package.json index 740a30e7..91526790 100644 --- a/package.json +++ b/package.json @@ -101,5 +101,6 @@ "tinypg": "^7.0.0", "tmp-promise": "^3.0.3", "uuid": "^10.0.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/__tests__/users.int.test.ts b/src/__tests__/users.int.test.ts index 624d2401..169cbf25 100644 --- a/src/__tests__/users.int.test.ts +++ b/src/__tests__/users.int.test.ts @@ -1,13 +1,24 @@ import request from 'supertest'; import { v4 as uuidv4 } from 'uuid'; import { app } from '../app'; -import { createUser, loadSystemUser, loadTableMetrics } from '../database'; +import { + createChangemaker, + createOrUpdateChangemakerRole, + createOrUpdateDataProvider, + createOrUpdateDataProviderRole, + createOrUpdateFunder, + createOrUpdateFunderRole, + createUser, + loadSystemUser, + loadTableMetrics, +} from '../database'; import { expectTimestamp, loadTestUser } from '../test/utils'; import { mockJwt as authHeader, mockJwtWithAdminRole as authHeaderWithAdminRole, } from '../test/mockJwt'; import { keycloakUserIdToString, stringToKeycloakUserId } from '../types'; +import { AccessType } from '../types/AccessType'; const createAdditionalTestUser = async () => createUser({ @@ -33,7 +44,84 @@ describe('/users', () => { .expect(200); expect(response.body).toEqual({ total: userCount, - entries: [testUser], + entries: [ + { + keycloakUserId: testUser.keycloakUserId, + roles: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, + createdAt: expectTimestamp, + }, + ], + }); + }); + + it('returns the roles associated with a user', async () => { + const systemUser = await loadSystemUser(); + const testUser = await loadTestUser(); + const dataProvider = await createOrUpdateDataProvider({ + name: 'Test Provider', + shortCode: 'testProvider', + }); + const funder = await createOrUpdateFunder({ + name: 'Test Funder', + shortCode: 'testFunder', + }); + const changemaker = await createChangemaker({ + name: 'Test Changemaker', + taxId: '12-3456789', + }); + await createOrUpdateDataProviderRole({ + dataProviderShortCode: dataProvider.shortCode, + userKeycloakUserId: testUser.keycloakUserId, + accessType: AccessType.MANAGE, + createdBy: systemUser.keycloakUserId, + }); + await createOrUpdateFunderRole({ + funderShortCode: funder.shortCode, + userKeycloakUserId: testUser.keycloakUserId, + accessType: AccessType.EDIT, + createdBy: systemUser.keycloakUserId, + }); + await createOrUpdateChangemakerRole({ + changemakerId: changemaker.id, + userKeycloakUserId: testUser.keycloakUserId, + accessType: AccessType.VIEW, + createdBy: systemUser.keycloakUserId, + }); + const { count: userCount } = await loadTableMetrics('users'); + + const response = await request(app) + .get('/users') + .set(authHeader) + .expect(200); + expect(response.body).toEqual({ + total: userCount, + entries: [ + { + keycloakUserId: testUser.keycloakUserId, + roles: { + changemaker: { + [changemaker.id]: { + view: true, + }, + }, + dataProvider: { + testProvider: { + manage: true, + }, + }, + funder: { + testFunder: { + edit: true, + }, + }, + }, + createdAt: expectTimestamp, + }, + ], }); }); @@ -99,22 +187,47 @@ describe('/users', () => { entries: [ { keycloakUserId: uuids[14], + roles: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[13], + roles: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[12], + roles: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[11], + roles: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[10], + roles: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, ], diff --git a/src/database/initialization/changemaker_role_to_json.sql b/src/database/initialization/changemaker_role_to_json.sql new file mode 100644 index 00000000..6a22034e --- /dev/null +++ b/src/database/initialization/changemaker_role_to_json.sql @@ -0,0 +1,14 @@ +SELECT drop_function('changemaker_role_to_json'); + +CREATE FUNCTION changemaker_role_to_json(changemaker_role changemaker_roles) +RETURNS JSONB AS $$ +BEGIN + RETURN jsonb_build_object( + 'userKeycloakUserId', changemaker_role.user_keycloak_user_id, + 'changemakerId', changemaker_role.changemaker_id, + 'accessType', changemaker_role.access_type, + 'createdBy', changemaker_role.created_by, + 'createdAt', changemaker_role.created_at + ); +END; +$$ LANGUAGE plpgsql; diff --git a/src/database/initialization/data_provider_role_to_json.sql b/src/database/initialization/data_provider_role_to_json.sql new file mode 100644 index 00000000..f060985b --- /dev/null +++ b/src/database/initialization/data_provider_role_to_json.sql @@ -0,0 +1,14 @@ +SELECT drop_function('data_provider_role_to_json'); + +CREATE FUNCTION data_provider_role_to_json(data_provider_role data_provider_roles) +RETURNS JSONB AS $$ +BEGIN + RETURN jsonb_build_object( + 'userKeycloakUserId', data_provider_role.user_keycloak_user_id, + 'dataProviderShortCode', data_provider_role.data_provider_short_code, + 'accessType', data_provider_role.access_type, + 'createdBy', data_provider_role.created_by, + 'createdAt', data_provider_role.created_at + ); +END; +$$ LANGUAGE plpgsql; diff --git a/src/database/initialization/funder_role_to_json.sql b/src/database/initialization/funder_role_to_json.sql new file mode 100644 index 00000000..5cf69415 --- /dev/null +++ b/src/database/initialization/funder_role_to_json.sql @@ -0,0 +1,14 @@ +SELECT drop_function('funder_role_to_json'); + +CREATE FUNCTION funder_role_to_json(funder_role funder_roles) +RETURNS JSONB AS $$ +BEGIN + RETURN jsonb_build_object( + 'userKeycloakUserId', funder_role.user_keycloak_user_id, + 'funderShortCode', funder_role.funder_short_code, + 'accessType', funder_role.access_type, + 'createdBy', funder_role.created_by, + 'createdAt', funder_role.created_at + ); +END; +$$ LANGUAGE plpgsql; diff --git a/src/database/initialization/user_to_json.sql b/src/database/initialization/user_to_json.sql index 23948c44..62aa3019 100644 --- a/src/database/initialization/user_to_json.sql +++ b/src/database/initialization/user_to_json.sql @@ -2,9 +2,57 @@ SELECT drop_function('user_to_json'); CREATE FUNCTION user_to_json("user" users) RETURNS JSONB AS $$ +DECLARE + roles_json JSONB := NULL::JSONB; + changemaker_roles_json JSONB := NULL::JSONB; + funder_roles_json JSONB := NULL::JSONB; + data_provider_roles_json JSONB := NULL::JSONB; BEGIN + changemaker_roles_json := ( + SELECT jsonb_object_agg( + changemaker_role_maps.changemaker_id, changemaker_role_maps.role_map + ) + FROM ( + SELECT changemaker_roles.changemaker_id, jsonb_object_agg(changemaker_roles.access_type, TRUE) AS role_map + FROM changemaker_roles + WHERE changemaker_roles.user_keycloak_user_id = "user".keycloak_user_id + GROUP BY changemaker_roles.changemaker_id + ) AS changemaker_role_maps + ); + + data_provider_roles_json := ( + SELECT jsonb_object_agg( + data_provider_role_maps.data_provider_short_code, data_provider_role_maps.role_map + ) + FROM ( + SELECT data_provider_roles.data_provider_short_code, jsonb_object_agg(data_provider_roles.access_type, TRUE) AS role_map + FROM data_provider_roles + WHERE data_provider_roles.user_keycloak_user_id = "user".keycloak_user_id + GROUP BY data_provider_roles.data_provider_short_code + ) AS data_provider_role_maps + ); + + funder_roles_json := ( + SELECT jsonb_object_agg( + funder_role_maps.funder_short_code, funder_role_maps.role_map + ) + FROM ( + SELECT funder_roles.funder_short_code, jsonb_object_agg(funder_roles.access_type, TRUE) AS role_map + FROM funder_roles + WHERE funder_roles.user_keycloak_user_id = "user".keycloak_user_id + GROUP BY funder_roles.funder_short_code + ) AS funder_role_maps + ); + + roles_json := jsonb_build_object( + 'changemaker', COALESCE(changemaker_roles_json, '{}'), + 'dataProvider', COALESCE(data_provider_roles_json, '{}'), + 'funder', COALESCE(funder_roles_json, '{}') + ); + RETURN jsonb_build_object( 'keycloakUserId', "user".keycloak_user_id, + 'roles', roles_json, 'createdAt', "user".created_at ); END; diff --git a/src/database/migrations/0040-create-changemaker_roles.sql b/src/database/migrations/0040-create-changemaker_roles.sql new file mode 100644 index 00000000..02f6fb74 --- /dev/null +++ b/src/database/migrations/0040-create-changemaker_roles.sql @@ -0,0 +1,17 @@ +CREATE TYPE access_type_t AS ENUM ( + 'manage', + 'edit', + 'view' +); + +CREATE TABLE changemaker_roles ( + user_keycloak_user_id UUID NOT NULL, + changemaker_id INT NOT NULL, + access_type access_type_t NOT NULL, + created_by UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_keycloak_user_id, changemaker_id, access_type), + FOREIGN KEY (created_by) REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + FOREIGN KEY (user_keycloak_user_id) REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + FOREIGN KEY (changemaker_id) REFERENCES changemakers(id) ON DELETE CASCADE +); diff --git a/src/database/migrations/0041-create-funder_roles.sql b/src/database/migrations/0041-create-funder_roles.sql new file mode 100644 index 00000000..c4d98e0f --- /dev/null +++ b/src/database/migrations/0041-create-funder_roles.sql @@ -0,0 +1,11 @@ +CREATE TABLE funder_roles ( + user_keycloak_user_id UUID NOT NULL, + funder_short_code short_code_t NOT NULL, + access_type access_type_t NOT NULL, + created_by UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_keycloak_user_id, funder_short_code, access_type), + FOREIGN KEY (created_by) REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + FOREIGN KEY (user_keycloak_user_id) REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + FOREIGN KEY (funder_short_code) REFERENCES funders(short_code) ON DELETE CASCADE +); diff --git a/src/database/migrations/0042-create-data_provider_roles.sql b/src/database/migrations/0042-create-data_provider_roles.sql new file mode 100644 index 00000000..b4435030 --- /dev/null +++ b/src/database/migrations/0042-create-data_provider_roles.sql @@ -0,0 +1,11 @@ +CREATE TABLE data_provider_roles ( + user_keycloak_user_id UUID NOT NULL, + data_provider_short_code short_code_t NOT NULL, + access_type access_type_t NOT NULL, + created_by UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_keycloak_user_id, data_provider_short_code, access_type), + FOREIGN KEY (created_by) REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + FOREIGN KEY (user_keycloak_user_id) REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + FOREIGN KEY (data_provider_short_code) REFERENCES data_providers(short_code) ON DELETE CASCADE +); diff --git a/src/database/operations/changemakerRoles/createOrUpdateChangemakerRole.ts b/src/database/operations/changemakerRoles/createOrUpdateChangemakerRole.ts new file mode 100644 index 00000000..f272af3c --- /dev/null +++ b/src/database/operations/changemakerRoles/createOrUpdateChangemakerRole.ts @@ -0,0 +1,32 @@ +import { db } from '../../db'; +import type { + DataProviderRole, + InternallyWritableChangemakerRole, + JsonResultSet, +} from '../../../types'; + +const createOrUpdateChangemakerRole = async ( + createValues: InternallyWritableChangemakerRole, +): Promise => { + const { userKeycloakUserId, changemakerId, accessType, createdBy } = + createValues; + const result = await db.sql>( + 'changemakerRoles.insertOrUpdateOne', + { + changemakerId, + userKeycloakUserId, + accessType, + createdBy, + }, + ); + + const { object } = result.rows[0] ?? {}; + if (object === undefined) { + throw new Error( + 'The entity creation did not appear to fail, but no data was returned by the operation.', + ); + } + return object; +}; + +export { createOrUpdateChangemakerRole }; diff --git a/src/database/operations/changemakerRoles/index.ts b/src/database/operations/changemakerRoles/index.ts new file mode 100644 index 00000000..0b0b800b --- /dev/null +++ b/src/database/operations/changemakerRoles/index.ts @@ -0,0 +1 @@ +export * from './createOrUpdateChangemakerRole'; diff --git a/src/database/operations/dataProviderRoles/createOrUpdateDataProviderRole.ts b/src/database/operations/dataProviderRoles/createOrUpdateDataProviderRole.ts new file mode 100644 index 00000000..469c382a --- /dev/null +++ b/src/database/operations/dataProviderRoles/createOrUpdateDataProviderRole.ts @@ -0,0 +1,32 @@ +import { db } from '../../db'; +import type { + DataProviderRole, + InternallyWritableDataProviderRole, + JsonResultSet, +} from '../../../types'; + +const createOrUpdateDataProviderRole = async ( + createValues: InternallyWritableDataProviderRole, +): Promise => { + const { userKeycloakUserId, dataProviderShortCode, accessType, createdBy } = + createValues; + const result = await db.sql>( + 'dataProviderRoles.insertOrUpdateOne', + { + dataProviderShortCode, + userKeycloakUserId, + accessType, + createdBy, + }, + ); + + const { object } = result.rows[0] ?? {}; + if (object === undefined) { + throw new Error( + 'The entity creation did not appear to fail, but no data was returned by the operation.', + ); + } + return object; +}; + +export { createOrUpdateDataProviderRole }; diff --git a/src/database/operations/dataProviderRoles/index.ts b/src/database/operations/dataProviderRoles/index.ts new file mode 100644 index 00000000..1f99c508 --- /dev/null +++ b/src/database/operations/dataProviderRoles/index.ts @@ -0,0 +1 @@ +export * from './createOrUpdateDataProviderRole'; diff --git a/src/database/operations/funderRoles/createOrUpdateFunderRole.ts b/src/database/operations/funderRoles/createOrUpdateFunderRole.ts new file mode 100644 index 00000000..d347c659 --- /dev/null +++ b/src/database/operations/funderRoles/createOrUpdateFunderRole.ts @@ -0,0 +1,32 @@ +import { db } from '../../db'; +import type { + FunderRole, + InternallyWritableFunderRole, + JsonResultSet, +} from '../../../types'; + +const createOrUpdateFunderRole = async ( + createValues: InternallyWritableFunderRole, +): Promise => { + const { userKeycloakUserId, funderShortCode, accessType, createdBy } = + createValues; + const result = await db.sql>( + 'funderRoles.insertOrUpdateOne', + { + funderShortCode, + userKeycloakUserId, + accessType, + createdBy, + }, + ); + + const { object } = result.rows[0] ?? {}; + if (object === undefined) { + throw new Error( + 'The entity creation did not appear to fail, but no data was returned by the operation.', + ); + } + return object; +}; + +export { createOrUpdateFunderRole }; diff --git a/src/database/operations/funderRoles/index.ts b/src/database/operations/funderRoles/index.ts new file mode 100644 index 00000000..14bcb0ed --- /dev/null +++ b/src/database/operations/funderRoles/index.ts @@ -0,0 +1 @@ +export * from './createOrUpdateFunderRole'; diff --git a/src/database/operations/index.ts b/src/database/operations/index.ts index f7d0c1cf..d5a679ac 100644 --- a/src/database/operations/index.ts +++ b/src/database/operations/index.ts @@ -4,8 +4,11 @@ export * from './baseFieldLocalization'; export * from './baseFields'; export * from './bulkUploads'; export * from './changemakerProposals'; +export * from './changemakerRoles'; export * from './changemakers'; +export * from './dataProviderRoles'; export * from './dataProviders'; +export * from './funderRoles'; export * from './funders'; export * from './generic'; export * from './opportunities'; diff --git a/src/database/queries/changemakerRoles/insertOrUpdateOne.sql b/src/database/queries/changemakerRoles/insertOrUpdateOne.sql new file mode 100644 index 00000000..47e606bc --- /dev/null +++ b/src/database/queries/changemakerRoles/insertOrUpdateOne.sql @@ -0,0 +1,14 @@ +INSERT INTO changemaker_roles ( + changemaker_id, + user_keycloak_user_id, + access_type, + created_by +) VALUES ( + :changemakerId, + :userKeycloakUserId, + :accessType::access_type_t, + :createdBy +) +ON CONFLICT (changemaker_id, user_keycloak_user_id, access_type) +DO NOTHING +RETURNING changemaker_role_to_json(changemaker_roles) AS "object"; diff --git a/src/database/queries/dataProviderRoles/insertOrUpdateOne.sql b/src/database/queries/dataProviderRoles/insertOrUpdateOne.sql new file mode 100644 index 00000000..7367ea0c --- /dev/null +++ b/src/database/queries/dataProviderRoles/insertOrUpdateOne.sql @@ -0,0 +1,19 @@ +INSERT INTO data_provider_roles ( + data_provider_short_code, + user_keycloak_user_id, + access_type, + created_by +) VALUES ( + :dataProviderShortCode, + :userKeycloakUserId, + :accessType::access_type_t, + :createdBy +) +ON CONFLICT (data_provider_short_code, user_keycloak_user_id, access_type) +DO NOTHING +RETURNING data_provider_role_to_json(data_provider_roles) AS "object"; + +-- DECISIONS: move assigned role to a better name, that reflects granted ability / accessType / etc and make it part of the PK +-- PUT /users/{userKeycloakUserId}/roles/funders/{shortCode}/accessType/administrator => {} +-- GET /users/{userKeycloakUserId}/roles/funders/{shortCode}/accessType/administrator +-- DELETE /users/{userKeycloakUserId}/roles/funders/{shortCode}/accessType/administrator diff --git a/src/database/queries/funderRoles/insertOrUpdateOne.sql b/src/database/queries/funderRoles/insertOrUpdateOne.sql new file mode 100644 index 00000000..3bd381bf --- /dev/null +++ b/src/database/queries/funderRoles/insertOrUpdateOne.sql @@ -0,0 +1,14 @@ +INSERT INTO funder_roles ( + funder_short_code, + user_keycloak_user_id, + access_type, + created_by +) VALUES ( + :funderShortCode, + :userKeycloakUserId, + :accessType::access_type_t, + :createdBy +) +ON CONFLICT (funder_short_code, user_keycloak_user_id, access_type) +DO NOTHING +RETURNING funder_role_to_json(funder_roles) AS "object"; diff --git a/src/openapi.json b/src/openapi.json index d5761457..2c9cb28f 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -947,6 +947,95 @@ } ] }, + "UserRole": { + "type": "string", + "enum": ["administrator", "editor", "viewer"] + }, + "ChangemakerRole": { + "type": "object", + "properties": { + "changemakerId": { + "type": "integer", + "readOnly": true + }, + "userKeycloakUserId": { + "type": "string", + "format": "uuid" + }, + "assignedRole": { + "$ref": "#/components/schemas/UserRole" + }, + "createdBy": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "required": ["changemakerId", "userKeycloakUserId", "assignedRole"] + }, + "DataProviderRole": { + "type": "object", + "properties": { + "dataProviderShortCode": { + "type": "string", + "readOnly": true + }, + "userKeycloakUserId": { + "type": "string", + "format": "uuid" + }, + "assignedRole": { + "$ref": "#/components/schemas/UserRole" + }, + "createdBy": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "required": [ + "dataProviderShortCode", + "userKeycloakUserId", + "assignedRole" + ] + }, + "FunderRole": { + "type": "object", + "properties": { + "funderShortCode": { + "type": "string", + "readOnly": true + }, + "userKeycloakUserId": { + "type": "string", + "format": "uuid" + }, + "assignedRole": { + "$ref": "#/components/schemas/UserRole" + }, + "createdBy": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "required": ["funderShortCode", "userKeycloakUserId", "assignedRole"] + }, "User": { "type": "object", "properties": { @@ -954,6 +1043,31 @@ "type": "string", "example": "550e8400-e29b-41d4-a716-446655440000" }, + "roles": { + "type": "object", + "readOnly": true, + "properties": { + "changemaker": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ChangemakerRole" + } + }, + "dataProvider": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/DataProviderRole" + } + }, + "funder": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/FunderRole" + } + } + }, + "required": ["changemaker", "dataProvider", "funder"] + }, "createdAt": { "type": "string", "format": "date-time", diff --git a/src/types/AccessType.ts b/src/types/AccessType.ts new file mode 100644 index 00000000..5fad7506 --- /dev/null +++ b/src/types/AccessType.ts @@ -0,0 +1,7 @@ +enum AccessType { + MANAGE = 'manage', + EDIT = 'edit', + VIEW = 'view', +} + +export { AccessType }; diff --git a/src/types/ChangemakerRole.ts b/src/types/ChangemakerRole.ts new file mode 100644 index 00000000..c42a1823 --- /dev/null +++ b/src/types/ChangemakerRole.ts @@ -0,0 +1,36 @@ +import { ajv } from '../ajv'; +import { AccessType } from './AccessType'; +import type { JSONSchemaType } from 'ajv'; +import type { Writable } from './Writable'; +import type { KeycloakUserId } from './KeycloakUserId'; + +interface ChangemakerRole { + readonly changemakerId: number; + readonly userKeycloakUserId: KeycloakUserId; + readonly accessType: AccessType; + readonly createdBy: KeycloakUserId; + readonly createdAt: string; +} + +type WritableChangemakerRole = Writable; + +type InternallyWritableChangemakerRole = WritableChangemakerRole & + Pick< + ChangemakerRole, + 'changemakerId' | 'userKeycloakUserId' | 'accessType' | 'createdBy' + >; + +const writableChangemakerRoleSchema: JSONSchemaType = { + type: 'object', + properties: {}, + required: [], +}; + +const isWritableChangemakerRole = ajv.compile(writableChangemakerRoleSchema); + +export { + ChangemakerRole, + InternallyWritableChangemakerRole, + isWritableChangemakerRole, + WritableChangemakerRole, +}; diff --git a/src/types/DataProviderRole.ts b/src/types/DataProviderRole.ts new file mode 100644 index 00000000..6d2567a5 --- /dev/null +++ b/src/types/DataProviderRole.ts @@ -0,0 +1,37 @@ +import { ajv } from '../ajv'; +import { AccessType } from './AccessType'; +import type { JSONSchemaType } from 'ajv'; +import type { Writable } from './Writable'; +import type { ShortCode } from './ShortCode'; +import type { KeycloakUserId } from './KeycloakUserId'; + +interface DataProviderRole { + readonly dataProviderShortCode: ShortCode; + readonly userKeycloakUserId: KeycloakUserId; + readonly accessType: AccessType; + readonly createdBy: KeycloakUserId; + readonly createdAt: string; +} + +type WritableDataProviderRole = Writable; + +type InternallyWritableDataProviderRole = WritableDataProviderRole & + Pick< + DataProviderRole, + 'dataProviderShortCode' | 'userKeycloakUserId' | 'accessType' | 'createdBy' + >; + +const writableDataProviderSchema: JSONSchemaType = { + type: 'object', + properties: {}, + required: [], +}; + +const isWritableDataProviderRole = ajv.compile(writableDataProviderSchema); + +export { + DataProviderRole, + InternallyWritableDataProviderRole, + isWritableDataProviderRole, + WritableDataProviderRole, +}; diff --git a/src/types/FunderRole.ts b/src/types/FunderRole.ts new file mode 100644 index 00000000..fba12003 --- /dev/null +++ b/src/types/FunderRole.ts @@ -0,0 +1,37 @@ +import { ajv } from '../ajv'; +import { AccessType } from './AccessType'; +import type { JSONSchemaType } from 'ajv'; +import type { Writable } from './Writable'; +import type { ShortCode } from './ShortCode'; +import type { KeycloakUserId } from './KeycloakUserId'; + +interface FunderRole { + readonly funderShortCode: ShortCode; + readonly userKeycloakUserId: KeycloakUserId; + readonly accessType: AccessType; + readonly createdBy: KeycloakUserId; + readonly createdAt: string; +} + +type WritableFunderRole = Writable; + +type InternallyWritableFunderRole = WritableFunderRole & + Pick< + FunderRole, + 'funderShortCode' | 'userKeycloakUserId' | 'accessType' | 'createdBy' + >; + +const writableFunderRoleSchema: JSONSchemaType = { + type: 'object', + properties: {}, + required: [], +}; + +const isWritableFunderRole = ajv.compile(writableFunderRoleSchema); + +export { + FunderRole, + InternallyWritableFunderRole, + isWritableFunderRole, + WritableFunderRole, +}; diff --git a/src/types/index.ts b/src/types/index.ts index 5ca7bc4b..91718b0f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,10 +7,13 @@ export * from './BulkUpload'; export * from './Bundle'; export * from './Changemaker'; export * from './ChangemakerProposal'; +export * from './ChangemakerRole'; export * from './CheckResult'; export * from './DataProvider'; +export * from './DataProviderRole'; export * from './express/AuthenticatedRequest'; export * from './Funder'; +export * from './FunderRole'; export * from './Id'; export * from './JsonObject'; export * from './JsonResultSet';