From e136838e36d9fd64951f77f84b38273992e1eceb Mon Sep 17 00:00:00 2001 From: Dan Schultz Date: Mon, 28 Oct 2024 10:03:13 -0400 Subject: [PATCH] Support user permissions Users in our system can be given permission to three kinds 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 + src/__tests__/users.int.test.ts | 116 +++++++++++++++- .../user_changemaker_permission_to_json.sql | 14 ++ .../user_data_provider_permission_to_json.sql | 14 ++ .../user_funder_permission_to_json.sql | 14 ++ src/database/initialization/user_to_json.sql | 47 +++++++ .../0040-create-permission-tables.sql | 32 +++++ src/database/operations/index.ts | 3 + ...createOrUpdateUserChangemakerPermission.ts | 32 +++++ .../userChangemakerPermissions/index.ts | 1 + ...reateOrUpdateUserDataProviderPermission.ts | 32 +++++ .../userDataProviderPermissions/index.ts | 1 + .../createOrUpdateUserFunderPermission.ts | 32 +++++ .../operations/userFunderPermissions/index.ts | 1 + .../insertOrUpdateOne.sql | 14 ++ .../insertOrUpdateOne.sql | 14 ++ .../insertOrUpdateOne.sql | 14 ++ src/openapi.json | 129 ++++++++++++++++++ src/types/Permission.ts | 7 + src/types/UserChangemakerPermission.ts | 40 ++++++ src/types/UserDataProviderPermission.ts | 44 ++++++ src/types/UserFunderPermission.ts | 40 ++++++ src/types/index.ts | 4 + 23 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 src/database/initialization/user_changemaker_permission_to_json.sql create mode 100644 src/database/initialization/user_data_provider_permission_to_json.sql create mode 100644 src/database/initialization/user_funder_permission_to_json.sql create mode 100644 src/database/migrations/0040-create-permission-tables.sql create mode 100644 src/database/operations/userChangemakerPermissions/createOrUpdateUserChangemakerPermission.ts create mode 100644 src/database/operations/userChangemakerPermissions/index.ts create mode 100644 src/database/operations/userDataProviderPermissions/createOrUpdateUserDataProviderPermission.ts create mode 100644 src/database/operations/userDataProviderPermissions/index.ts create mode 100644 src/database/operations/userFunderPermissions/createOrUpdateUserFunderPermission.ts create mode 100644 src/database/operations/userFunderPermissions/index.ts create mode 100644 src/database/queries/userChangemakerPermissions/insertOrUpdateOne.sql create mode 100644 src/database/queries/userDataProviderPermissions/insertOrUpdateOne.sql create mode 100644 src/database/queries/userFunderPermissions/insertOrUpdateOne.sql create mode 100644 src/types/Permission.ts create mode 100644 src/types/UserChangemakerPermission.ts create mode 100644 src/types/UserDataProviderPermission.ts create mode 100644 src/types/UserFunderPermission.ts diff --git a/docs/ENTITY_RELATIONSHIP_DIAGRAM.md b/docs/ENTITY_RELATIONSHIP_DIAGRAM.md index 0e25a99a..ae4932eb 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 granted permissions for" + User }o--o{ Funder : "is granted permissions for" + User }o--o{ DataProvider : "is granted permissions for" ``` ## Narrative diff --git a/src/__tests__/users.int.test.ts b/src/__tests__/users.int.test.ts index 624d2401..f296eebe 100644 --- a/src/__tests__/users.int.test.ts +++ b/src/__tests__/users.int.test.ts @@ -1,13 +1,27 @@ import request from 'supertest'; import { v4 as uuidv4 } from 'uuid'; import { app } from '../app'; -import { createUser, loadSystemUser, loadTableMetrics } from '../database'; +import { + createChangemaker, + createOrUpdateDataProvider, + createOrUpdateFunder, + createOrUpdateUserChangemakerPermission, + createOrUpdateUserDataProviderPermission, + createOrUpdateUserFunderPermission, + 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 { + keycloakUserIdToString, + stringToKeycloakUserId, + Permission, +} from '../types'; const createAdditionalTestUser = async () => createUser({ @@ -33,7 +47,78 @@ describe('/users', () => { .expect(200); expect(response.body).toEqual({ total: userCount, - entries: [testUser], + entries: [ + { + keycloakUserId: testUser.keycloakUserId, + permissions: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, + createdAt: expectTimestamp, + }, + ], + }); + }); + + it('returns the permissions 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 createOrUpdateUserDataProviderPermission({ + userKeycloakUserId: testUser.keycloakUserId, + permission: Permission.MANAGE, + dataProviderShortCode: dataProvider.shortCode, + createdBy: systemUser.keycloakUserId, + }); + await createOrUpdateUserFunderPermission({ + userKeycloakUserId: testUser.keycloakUserId, + permission: Permission.EDIT, + funderShortCode: funder.shortCode, + createdBy: systemUser.keycloakUserId, + }); + await createOrUpdateUserChangemakerPermission({ + userKeycloakUserId: testUser.keycloakUserId, + permission: Permission.VIEW, + changemakerId: changemaker.id, + 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, + permissions: { + changemaker: { + [changemaker.id]: [Permission.VIEW], + }, + dataProvider: { + testProvider: [Permission.MANAGE], + }, + funder: { + testFunder: [Permission.EDIT], + }, + }, + createdAt: expectTimestamp, + }, + ], }); }); @@ -99,22 +184,47 @@ describe('/users', () => { entries: [ { keycloakUserId: uuids[14], + permissions: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[13], + permissions: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[12], + permissions: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[11], + permissions: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, { keycloakUserId: uuids[10], + permissions: { + changemaker: {}, + dataProvider: {}, + funder: {}, + }, createdAt: expectTimestamp, }, ], diff --git a/src/database/initialization/user_changemaker_permission_to_json.sql b/src/database/initialization/user_changemaker_permission_to_json.sql new file mode 100644 index 00000000..1dc66f7d --- /dev/null +++ b/src/database/initialization/user_changemaker_permission_to_json.sql @@ -0,0 +1,14 @@ +SELECT drop_function('user_changemaker_permission_to_json'); + +CREATE FUNCTION user_changemaker_permission_to_json(user_changemaker_permission user_changemaker_permissions) +RETURNS JSONB AS $$ +BEGIN + RETURN jsonb_build_object( + 'userKeycloakUserId', user_changemaker_permission.user_keycloak_user_id, + 'permission', user_changemaker_permission.permission, + 'changemakerId', user_changemaker_permission.changemaker_id, + 'createdBy', user_changemaker_permission.created_by, + 'createdAt', user_changemaker_permission.created_at + ); +END; +$$ LANGUAGE plpgsql; diff --git a/src/database/initialization/user_data_provider_permission_to_json.sql b/src/database/initialization/user_data_provider_permission_to_json.sql new file mode 100644 index 00000000..f64b5807 --- /dev/null +++ b/src/database/initialization/user_data_provider_permission_to_json.sql @@ -0,0 +1,14 @@ +SELECT drop_function('user_data_provider_permission_to_json'); + +CREATE FUNCTION user_data_provider_permission_to_json(user_data_provider_permission user_data_provider_permissions) +RETURNS JSONB AS $$ +BEGIN + RETURN jsonb_build_object( + 'userKeycloakUserId', user_data_provider_permission.user_keycloak_user_id, + 'permission', user_data_provider_permission.permission, + 'dataProviderShortCode', user_data_provider_permission.data_provider_short_code, + 'createdBy', user_data_provider_permission.created_by, + 'createdAt', user_data_provider_permission.created_at + ); +END; +$$ LANGUAGE plpgsql; diff --git a/src/database/initialization/user_funder_permission_to_json.sql b/src/database/initialization/user_funder_permission_to_json.sql new file mode 100644 index 00000000..bca0dad0 --- /dev/null +++ b/src/database/initialization/user_funder_permission_to_json.sql @@ -0,0 +1,14 @@ +SELECT drop_function('user_funder_permission_to_json'); + +CREATE FUNCTION user_funder_permission_to_json(user_funder_permission user_funder_permissions) +RETURNS JSONB AS $$ +BEGIN + RETURN jsonb_build_object( + 'userKeycloakUserId', user_funder_permission.user_keycloak_user_id, + 'permission', user_funder_permission.permission, + 'funderShortCode', user_funder_permission.funder_short_code, + 'createdBy', user_funder_permission.created_by, + 'createdAt', user_funder_permission.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..59fc0e2a 100644 --- a/src/database/initialization/user_to_json.sql +++ b/src/database/initialization/user_to_json.sql @@ -2,9 +2,56 @@ SELECT drop_function('user_to_json'); CREATE FUNCTION user_to_json("user" users) RETURNS JSONB AS $$ +DECLARE + permissions_json JSONB := NULL::JSONB; + user_changemaker_permissions_json JSONB := NULL::JSONB; + user_funder_permissions_json JSONB := NULL::JSONB; + user_data_provider_permissions_json JSONB := NULL::JSONB; BEGIN + user_changemaker_permissions_json := ( + SELECT jsonb_object_agg( + aggregated_user_changemaker_permissions.changemaker_id, aggregated_user_changemaker_permissions.permissions + ) + FROM ( + SELECT user_changemaker_permissions.changemaker_id, jsonb_agg(user_changemaker_permissions.permission) AS permissions + FROM user_changemaker_permissions + WHERE user_changemaker_permissions.user_keycloak_user_id = "user".keycloak_user_id + GROUP BY user_changemaker_permissions.changemaker_id + ) AS aggregated_user_changemaker_permissions + ); + + user_data_provider_permissions_json := ( + SELECT jsonb_object_agg( + aggregated_user_data_provider_permissions.data_provider_short_code, aggregated_user_data_provider_permissions.permissions + ) + FROM ( + SELECT user_data_provider_permissions.data_provider_short_code, jsonb_agg(user_data_provider_permissions.permission) AS permissions + FROM user_data_provider_permissions + WHERE user_data_provider_permissions.user_keycloak_user_id = "user".keycloak_user_id + GROUP BY user_data_provider_permissions.data_provider_short_code + ) AS aggregated_user_data_provider_permissions ); + + user_funder_permissions_json := ( + SELECT jsonb_object_agg( + aggregated_user_funder_permissions.funder_short_code, aggregated_user_funder_permissions.permissions + ) + FROM ( + SELECT user_funder_permissions.funder_short_code, jsonb_agg(user_funder_permissions.permission) AS permissions + FROM user_funder_permissions + WHERE user_funder_permissions.user_keycloak_user_id = "user".keycloak_user_id + GROUP BY user_funder_permissions.funder_short_code + ) AS aggregated_user_funder_permissions + ); + + permissions_json := jsonb_build_object( + 'changemaker', COALESCE(user_changemaker_permissions_json, '{}'), + 'dataProvider', COALESCE(user_data_provider_permissions_json, '{}'), + 'funder', COALESCE(user_funder_permissions_json, '{}') + ); + RETURN jsonb_build_object( 'keycloakUserId', "user".keycloak_user_id, + 'permissions', permissions_json, 'createdAt', "user".created_at ); END; diff --git a/src/database/migrations/0040-create-permission-tables.sql b/src/database/migrations/0040-create-permission-tables.sql new file mode 100644 index 00000000..26f2172c --- /dev/null +++ b/src/database/migrations/0040-create-permission-tables.sql @@ -0,0 +1,32 @@ +CREATE TYPE permission_t AS ENUM ( + 'manage', + 'edit', + 'view' +); + +CREATE TABLE user_changemaker_permissions ( + user_keycloak_user_id UUID NOT NULL REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + permission permission_t NOT NULL, + changemaker_id INT NOT NULL REFERENCES changemakers(id) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_keycloak_user_id, permission, changemaker_id) +); + +CREATE TABLE user_funder_permissions ( + user_keycloak_user_id UUID NOT NULL REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + permission permission_t NOT NULL, + funder_short_code short_code_t NOT NULL REFERENCES funders(short_code) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_keycloak_user_id, permission, funder_short_code) +); + +CREATE TABLE user_data_provider_permissions ( + user_keycloak_user_id UUID NOT NULL REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + permission permission_t NOT NULL, + data_provider_short_code short_code_t NOT NULL REFERENCES data_providers(short_code) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES users(keycloak_user_id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_keycloak_user_id, permission, data_provider_short_code) +); diff --git a/src/database/operations/index.ts b/src/database/operations/index.ts index f7d0c1cf..4dd103dc 100644 --- a/src/database/operations/index.ts +++ b/src/database/operations/index.ts @@ -13,4 +13,7 @@ export * from './proposalFieldValues'; export * from './proposals'; export * from './proposalVersions'; export * from './sources'; +export * from './userChangemakerPermissions'; +export * from './userDataProviderPermissions'; +export * from './userFunderPermissions'; export * from './users'; diff --git a/src/database/operations/userChangemakerPermissions/createOrUpdateUserChangemakerPermission.ts b/src/database/operations/userChangemakerPermissions/createOrUpdateUserChangemakerPermission.ts new file mode 100644 index 00000000..d3ac53f7 --- /dev/null +++ b/src/database/operations/userChangemakerPermissions/createOrUpdateUserChangemakerPermission.ts @@ -0,0 +1,32 @@ +import { db } from '../../db'; +import type { + InternallyWritableUserChangemakerPermission, + JsonResultSet, + UserChangemakerPermission, +} from '../../../types'; + +const createOrUpdateUserChangemakerPermission = async ( + createValues: InternallyWritableUserChangemakerPermission, +): Promise => { + const { userKeycloakUserId, changemakerId, permission, createdBy } = + createValues; + const result = await db.sql>( + 'userChangemakerPermissions.insertOrUpdateOne', + { + userKeycloakUserId, + permission, + changemakerId, + 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 { createOrUpdateUserChangemakerPermission }; diff --git a/src/database/operations/userChangemakerPermissions/index.ts b/src/database/operations/userChangemakerPermissions/index.ts new file mode 100644 index 00000000..5bce400c --- /dev/null +++ b/src/database/operations/userChangemakerPermissions/index.ts @@ -0,0 +1 @@ +export * from './createOrUpdateUserChangemakerPermission'; diff --git a/src/database/operations/userDataProviderPermissions/createOrUpdateUserDataProviderPermission.ts b/src/database/operations/userDataProviderPermissions/createOrUpdateUserDataProviderPermission.ts new file mode 100644 index 00000000..10e00d12 --- /dev/null +++ b/src/database/operations/userDataProviderPermissions/createOrUpdateUserDataProviderPermission.ts @@ -0,0 +1,32 @@ +import { db } from '../../db'; +import type { + UserDataProviderPermission, + InternallyWritableUserDataProviderPermission, + JsonResultSet, +} from '../../../types'; + +const createOrUpdateUserDataProviderPermission = async ( + createValues: InternallyWritableUserDataProviderPermission, +): Promise => { + const { userKeycloakUserId, dataProviderShortCode, permission, createdBy } = + createValues; + const result = await db.sql>( + 'userDataProviderPermissions.insertOrUpdateOne', + { + userKeycloakUserId, + permission, + dataProviderShortCode, + 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 { createOrUpdateUserDataProviderPermission }; diff --git a/src/database/operations/userDataProviderPermissions/index.ts b/src/database/operations/userDataProviderPermissions/index.ts new file mode 100644 index 00000000..dd63d340 --- /dev/null +++ b/src/database/operations/userDataProviderPermissions/index.ts @@ -0,0 +1 @@ +export * from './createOrUpdateUserDataProviderPermission'; diff --git a/src/database/operations/userFunderPermissions/createOrUpdateUserFunderPermission.ts b/src/database/operations/userFunderPermissions/createOrUpdateUserFunderPermission.ts new file mode 100644 index 00000000..065e80ff --- /dev/null +++ b/src/database/operations/userFunderPermissions/createOrUpdateUserFunderPermission.ts @@ -0,0 +1,32 @@ +import { db } from '../../db'; +import type { + UserFunderPermission, + InternallyWritableUserFunderPermission, + JsonResultSet, +} from '../../../types'; + +const createOrUpdateUserFunderPermission = async ( + createValues: InternallyWritableUserFunderPermission, +): Promise => { + const { userKeycloakUserId, funderShortCode, permission, createdBy } = + createValues; + const result = await db.sql>( + 'userFunderPermissions.insertOrUpdateOne', + { + userKeycloakUserId, + permission, + funderShortCode, + 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 { createOrUpdateUserFunderPermission }; diff --git a/src/database/operations/userFunderPermissions/index.ts b/src/database/operations/userFunderPermissions/index.ts new file mode 100644 index 00000000..f0a3a374 --- /dev/null +++ b/src/database/operations/userFunderPermissions/index.ts @@ -0,0 +1 @@ +export * from './createOrUpdateUserFunderPermission'; diff --git a/src/database/queries/userChangemakerPermissions/insertOrUpdateOne.sql b/src/database/queries/userChangemakerPermissions/insertOrUpdateOne.sql new file mode 100644 index 00000000..f97e31a1 --- /dev/null +++ b/src/database/queries/userChangemakerPermissions/insertOrUpdateOne.sql @@ -0,0 +1,14 @@ +INSERT INTO user_changemaker_permissions ( + user_keycloak_user_id, + permission, + changemaker_id, + created_by +) VALUES ( + :userKeycloakUserId, + :permission::permission_t, + :changemakerId, + :createdBy +) +ON CONFLICT (user_keycloak_user_id, permission, changemaker_id) +DO NOTHING +RETURNING user_changemaker_permission_to_json(user_changemaker_permissions) AS "object"; diff --git a/src/database/queries/userDataProviderPermissions/insertOrUpdateOne.sql b/src/database/queries/userDataProviderPermissions/insertOrUpdateOne.sql new file mode 100644 index 00000000..85486fc4 --- /dev/null +++ b/src/database/queries/userDataProviderPermissions/insertOrUpdateOne.sql @@ -0,0 +1,14 @@ +INSERT INTO user_data_provider_permissions ( + user_keycloak_user_id, + permission, + data_provider_short_code, + created_by +) VALUES ( + :userKeycloakUserId, + :permission::permission_t, + :dataProviderShortCode, + :createdBy +) +ON CONFLICT (user_keycloak_user_id, permission, data_provider_short_code) +DO NOTHING +RETURNING user_data_provider_permission_to_json(user_data_provider_permissions) AS "object"; diff --git a/src/database/queries/userFunderPermissions/insertOrUpdateOne.sql b/src/database/queries/userFunderPermissions/insertOrUpdateOne.sql new file mode 100644 index 00000000..984939b6 --- /dev/null +++ b/src/database/queries/userFunderPermissions/insertOrUpdateOne.sql @@ -0,0 +1,14 @@ +INSERT INTO user_funder_permissions ( + user_keycloak_user_id, + permission, + funder_short_code, + created_by +) VALUES ( + :userKeycloakUserId, + :permission::permission_t, + :funderShortCode, + :createdBy +) +ON CONFLICT (user_keycloak_user_id, permission, funder_short_code) +DO NOTHING +RETURNING user_funder_permission_to_json(user_funder_permissions) AS "object"; diff --git a/src/openapi.json b/src/openapi.json index d5761457..d32cac96 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -947,6 +947,110 @@ } ] }, + "AccessType": { + "type": "string", + "enum": ["manage", "edit", "view"] + }, + "ChangemakerRole": { + "type": "object", + "properties": { + "changemakerId": { + "type": "integer", + "readOnly": true + }, + "userKeycloakUserId": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "accessType": { + "$ref": "#/components/schemas/AccessType", + "readOnly": true + }, + "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", + "readOnly": true + }, + "accessType": { + "$ref": "#/components/schemas/AccessType", + "readOnly": true + }, + "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", + "readOnly": true + }, + "accessType": { + "$ref": "#/components/schemas/AccessType", + "readOnly": true + }, + "createdBy": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "required": ["funderShortCode", "userKeycloakUserId", "assignedRole"] + }, + "RoleMap": { + "type": "object", + "propertyNames": { + "$ref": "#/components/schemas/AccessType" + }, + "additionalProperties": { + "type": "boolean" + } + }, "User": { "type": "object", "properties": { @@ -954,6 +1058,31 @@ "type": "string", "example": "550e8400-e29b-41d4-a716-446655440000" }, + "roles": { + "type": "object", + "readOnly": true, + "properties": { + "changemaker": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RoleMap" + } + }, + "dataProvider": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RoleMap" + } + }, + "funder": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RoleMap" + } + } + }, + "required": ["changemaker", "dataProvider", "funder"] + }, "createdAt": { "type": "string", "format": "date-time", diff --git a/src/types/Permission.ts b/src/types/Permission.ts new file mode 100644 index 00000000..ebab4002 --- /dev/null +++ b/src/types/Permission.ts @@ -0,0 +1,7 @@ +enum Permission { + MANAGE = 'manage', + EDIT = 'edit', + VIEW = 'view', +} + +export { Permission }; diff --git a/src/types/UserChangemakerPermission.ts b/src/types/UserChangemakerPermission.ts new file mode 100644 index 00000000..0020968c --- /dev/null +++ b/src/types/UserChangemakerPermission.ts @@ -0,0 +1,40 @@ +import { ajv } from '../ajv'; +import { Permission } from './Permission'; +import type { JSONSchemaType } from 'ajv'; +import type { Writable } from './Writable'; +import type { KeycloakUserId } from './KeycloakUserId'; + +interface UserChangemakerPermission { + readonly userKeycloakUserId: KeycloakUserId; + readonly permission: Permission; + readonly changemakerId: number; + readonly createdBy: KeycloakUserId; + readonly createdAt: string; +} + +type WritableUserChangemakerPermission = Writable; + +type InternallyWritableUserChangemakerPermission = + WritableUserChangemakerPermission & + Pick< + UserChangemakerPermission, + 'userKeycloakUserId' | 'permission' | 'changemakerId' | 'createdBy' + >; + +const writableUserChangemakerPermissionSchema: JSONSchemaType = + { + type: 'object', + properties: {}, + required: [], + }; + +const isWritableUserChangemakerPermission = ajv.compile( + writableUserChangemakerPermissionSchema, +); + +export { + InternallyWritableUserChangemakerPermission, + UserChangemakerPermission, + WritableUserChangemakerPermission, + isWritableUserChangemakerPermission, +}; diff --git a/src/types/UserDataProviderPermission.ts b/src/types/UserDataProviderPermission.ts new file mode 100644 index 00000000..df706478 --- /dev/null +++ b/src/types/UserDataProviderPermission.ts @@ -0,0 +1,44 @@ +import { ajv } from '../ajv'; +import { Permission } from './Permission'; +import type { JSONSchemaType } from 'ajv'; +import type { Writable } from './Writable'; +import type { ShortCode } from './ShortCode'; +import type { KeycloakUserId } from './KeycloakUserId'; + +interface UserDataProviderPermission { + readonly userKeycloakUserId: KeycloakUserId; + readonly permission: Permission; + readonly dataProviderShortCode: ShortCode; + readonly createdBy: KeycloakUserId; + readonly createdAt: string; +} + +type WritableUserDataProviderPermission = Writable; + +type InternallyWritableUserDataProviderPermission = + WritableUserDataProviderPermission & + Pick< + UserDataProviderPermission, + | 'userKeycloakUserId' + | 'permission' + | 'dataProviderShortCode' + | 'createdBy' + >; + +const writableUserDataProviderSchema: JSONSchemaType = + { + type: 'object', + properties: {}, + required: [], + }; + +const isWritableUserDataProviderPermission = ajv.compile( + writableUserDataProviderSchema, +); + +export { + InternallyWritableUserDataProviderPermission, + UserDataProviderPermission, + WritableUserDataProviderPermission, + isWritableUserDataProviderPermission, +}; diff --git a/src/types/UserFunderPermission.ts b/src/types/UserFunderPermission.ts new file mode 100644 index 00000000..7be9a268 --- /dev/null +++ b/src/types/UserFunderPermission.ts @@ -0,0 +1,40 @@ +import { ajv } from '../ajv'; +import { Permission } from './Permission'; +import type { JSONSchemaType } from 'ajv'; +import type { Writable } from './Writable'; +import type { ShortCode } from './ShortCode'; +import type { KeycloakUserId } from './KeycloakUserId'; + +interface UserFunderPermission { + readonly userKeycloakUserId: KeycloakUserId; + readonly permission: Permission; + readonly funderShortCode: ShortCode; + readonly createdBy: KeycloakUserId; + readonly createdAt: string; +} + +type WritableUserFunderPermission = Writable; + +type InternallyWritableUserFunderPermission = WritableUserFunderPermission & + Pick< + UserFunderPermission, + 'userKeycloakUserId' | 'permission' | 'funderShortCode' | 'createdBy' + >; + +const writableUserFunderPermissionSchema: JSONSchemaType = + { + type: 'object', + properties: {}, + required: [], + }; + +const isWritableUserFunderPermission = ajv.compile( + writableUserFunderPermissionSchema, +); + +export { + InternallyWritableUserFunderPermission, + UserFunderPermission, + WritableUserFunderPermission, + isWritableUserFunderPermission, +}; diff --git a/src/types/index.ts b/src/types/index.ts index 5ca7bc4b..4787af3e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,6 +19,7 @@ export * from './Language'; export * from './Opportunity'; export * from './PaginationParameters'; export * from './PaginationParametersQuery'; +export * from './Permission'; export * from './PlatformProviderResponse'; export * from './PostgresErrorCode'; export * from './PresignedPostRequest'; @@ -31,4 +32,7 @@ export * from './Source'; export * from './TableMetrics'; export * from './TinyPgErrorWithQueryContext'; export * from './User'; +export * from './UserChangemakerPermission'; +export * from './UserDataProviderPermission'; +export * from './UserFunderPermission'; export * from './Uuid';