diff --git a/packages/integration-tests/src/tests/api/organization-user.test.ts b/packages/integration-tests/src/tests/api/organization-user.test.ts index 962209526..2c5d98664 100644 --- a/packages/integration-tests/src/tests/api/organization-user.test.ts +++ b/packages/integration-tests/src/tests/api/organization-user.test.ts @@ -227,5 +227,24 @@ describe('organization user APIs', () => { expect(user2Roles).toContainEqual(expect.objectContaining({ id: role1.id })); expect(user2Roles).toContainEqual(expect.objectContaining({ id: role2.id })); }); + + it('should automatically remove all roles when remove a user from an organization', async () => { + const organization = await organizationApi.create({ name: 'test' }); + const user = await userApi.create({ username: generateTestName() }); + const [role1, role2] = await Promise.all([ + roleApi.create({ name: generateTestName() }), + roleApi.create({ name: generateTestName() }), + ]); + + await organizationApi.addUsers(organization.id, [user.id]); + await organizationApi.addUserRoles(organization.id, user.id, [role1.id, role2.id]); + expect(await organizationApi.getUserRoles(organization.id, user.id)).toHaveLength(2); + + await organizationApi.deleteUser(organization.id, user.id); + const response = await organizationApi + .getUserRoles(organization.id, user.id) + .catch((error: unknown) => error); + expect(response instanceof HTTPError && response.response.statusCode).toBe(422); // Require membership + }); }); }); diff --git a/packages/schemas/alterations/next-1700031616-update-org-role-foreign-keys.ts b/packages/schemas/alterations/next-1700031616-update-org-role-foreign-keys.ts new file mode 100644 index 000000000..8dd517951 --- /dev/null +++ b/packages/schemas/alterations/next-1700031616-update-org-role-foreign-keys.ts @@ -0,0 +1,35 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table organization_role_user_relations + drop constraint organization_role_user_relations_organization_id_fkey; + alter table organization_role_user_relations + drop constraint organization_role_user_relations_user_id_fkey; + alter table organization_role_user_relations + add foreign key (tenant_id, organization_id, user_id) + references organization_user_relations (tenant_id, organization_id, user_id) + on update cascade on delete cascade; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table organization_role_user_relations + -- The constraint name is strange because it's generated by Postgres and it has a 63 character limit + drop constraint organization_role_user_relati_tenant_id_organization_id_us_fkey; + alter table organization_role_user_relations + add foreign key (organization_id) + references organizations (id) + on update cascade on delete cascade; + alter table organization_role_user_relations + add foreign key (user_id) + references users (id) + on update cascade on delete cascade; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/tables/organization_role_user_relations.sql b/packages/schemas/tables/organization_role_user_relations.sql index ffbfad66f..8a6cdc6a7 100644 --- a/packages/schemas/tables/organization_role_user_relations.sql +++ b/packages/schemas/tables/organization_role_user_relations.sql @@ -1,14 +1,16 @@ -/* init_order = 2 */ +/* init_order = 3 */ /** The relations between organizations, organization roles, and users. A relation means that a user has a role in an organization. */ create table organization_role_user_relations ( tenant_id varchar(21) not null references tenants (id) on update cascade on delete cascade, - organization_id varchar(21) not null - references organizations (id) on update cascade on delete cascade, + organization_id varchar(21) not null, organization_role_id varchar(21) not null references organization_roles (id) on update cascade on delete cascade, - user_id varchar(21) not null - references users (id) on update cascade on delete cascade, - primary key (tenant_id, organization_id, organization_role_id, user_id) + user_id varchar(21) not null, + primary key (tenant_id, organization_id, organization_role_id, user_id), + /** User's roles in an organization should be synchronized with the user's membership in the organization. */ + foreign key (tenant_id, organization_id, user_id) + references organization_user_relations (tenant_id, organization_id, user_id) + on update cascade on delete cascade );