Skip to content

Commit

Permalink
SCIM: add groups.type migration
Browse files Browse the repository at this point in the history
  • Loading branch information
fflorent committed Jan 3, 2025
1 parent 546feb5 commit fbce58e
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 45 deletions.
6 changes: 6 additions & 0 deletions app/gen-server/entity/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {User} from "./User";

@Entity({name: 'groups'})
export class Group extends BaseEntity {
public static readonly ROLE_TYPE = 'role';
public static readonly RESOURCE_USERS_TYPE = 'resource users';

@PrimaryGeneratedColumn()
public id: number;
Expand All @@ -30,4 +32,8 @@ export class Group extends BaseEntity {

@OneToOne(type => AclRule, aclRule => aclRule.group)
public aclRule: AclRule;

@Column({type: String, enum: [Group.ROLE_TYPE, Group.RESOURCE_USERS_TYPE], default: Group.ROLE_TYPE, select: false,
nullable: true})
public type: typeof Group.ROLE_TYPE | typeof Group.RESOURCE_USERS_TYPE = Group.ROLE_TYPE;
}
2 changes: 2 additions & 0 deletions app/gen-server/lib/homedb/GroupsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Workspace } from "app/gen-server/entity/Workspace";

import { EntityManager } from "typeorm";

export type GroupTypes = typeof Group.ROLE_TYPE | typeof Group.RESOURCE_USERS_TYPE;

/**
* Class responsible for Groups and Roles Management.
*
Expand Down
33 changes: 33 additions & 0 deletions app/gen-server/migration/1734097274107-GroupTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { Group } from "app/gen-server/entity/Group";

export class GroupTypes1734097274107 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<any> {
const newColumn = new TableColumn({
name: 'type',
type: 'string',
enum: [Group.ROLE_TYPE, Group.RESOURCE_USERS_TYPE],
comment: `If the type is ${Group.ROLE_TYPE}, the group is meant to assign a role to ` +
'users for a resource (document, workspace or org).' +
'\n\n' +
`If the type is "${Group.RESOURCE_USERS_TYPE}", the group is meant to gather users together ` +
'so they can be granted the same role to some resources (hence this name).',
isNullable: true, // Make it not nullable after setting the roles for existing groups
});

await queryRunner.addColumn('groups', newColumn);

await queryRunner.manager
.query('UPDATE groups SET type = $1', [Group.ROLE_TYPE]);

newColumn.isNullable = false;

await queryRunner.changeColumn('groups', newColumn.name, newColumn);
}

public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropColumn('groups', 'type');
}
}

103 changes: 58 additions & 45 deletions test/gen-server/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import {ActivationEnabled1722529827161
import {Configs1727747249153 as Configs} from 'app/gen-server/migration/1727747249153-Configs';
import {LoginsEmailsIndex1729754662550
as LoginsEmailsIndex} from 'app/gen-server/migration/1729754662550-LoginsEmailIndex';
import {GroupTypes1734097274107
as GroupTypes} from 'app/gen-server/migration/1734097274107-GroupTypes';

const home: HomeDBManager = new HomeDBManager();

Expand All @@ -58,7 +60,7 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE
ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart,
DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID,
Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures,
UserLastConnection, ActivationEnabled, Configs, LoginsEmailsIndex];
UserLastConnection, ActivationEnabled, Configs, LoginsEmailsIndex, GroupTypes];

// Assert that the "members" acl rule and group exist (or not).
function assertMembersGroup(org: Organization, exists: boolean) {
Expand Down Expand Up @@ -150,55 +152,55 @@ describe('migrations', function() {

it('can correctly switch display_email column to non-null with data', async function() {
this.timeout(60000);
const sqlite = home.connection.driver.options.type === 'sqlite';
// sqlite migrations need foreign keys turned off temporarily
if (sqlite) { await home.connection.query("PRAGMA foreign_keys = OFF;"); }
const runner = home.connection.createQueryRunner();
for (const migration of migrations) {
await (new migration()).up(runner);
}
await addSeedData(home.connection);
// migrate back until just before display_email column added, so we have no
// display_emails
for (const migration of migrations.slice().reverse()) {
await (new migration()).down(runner);
if (migration.name === DisplayEmail.name) { break; }
}
// now check DisplayEmail and DisplayEmailNonNull succeed with data in the db.
await (new DisplayEmail()).up(runner);
await (new DisplayEmailNonNull()).up(runner);
if (sqlite) { await home.connection.query("PRAGMA foreign_keys = ON;"); }
return disableSqliteForeignKey(async () => {
const runner = home.connection.createQueryRunner();
for (const migration of migrations) {
await (new migration()).up(runner);
}
await addSeedData(home.connection);
// migrate back until just before display_email column added, so we have no
// display_emails
for (const migration of migrations.slice().reverse()) {
await (new migration()).down(runner);
if (migration.name === DisplayEmail.name) { break; }
}
// now check DisplayEmail and DisplayEmailNonNull succeed with data in the db.
await (new DisplayEmail()).up(runner);
await (new DisplayEmailNonNull()).up(runner);
});
});

// a test to ensure the TeamMember migration works on databases with existing content
it('can perform TeamMember migration with seed data set', async function() {
this.timeout(30000);
const runner = home.connection.createQueryRunner();
// Perform full up migration and add the seed data.
for (const migration of migrations) {
await (new migration()).up(runner);
}
await addSeedData(home.connection);
const initAclCount = await getAclRowCount(runner);
const initGroupCount = await getGroupRowCount(runner);

// Assert that members groups are present to start.
for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); }

// Perform down TeamMembers migration with seed data and assert members groups are removed.
await (new TeamMembers()).down(runner);
const downMigratedOrgs = await getAllOrgs(runner);
for (const org of downMigratedOrgs) { assertMembersGroup(org, false); }
// Assert that the correct number of ACLs and groups were removed.
assert.equal(await getAclRowCount(runner), initAclCount - downMigratedOrgs.length);
assert.equal(await getGroupRowCount(runner), initGroupCount - downMigratedOrgs.length);

// Perform up TeamMembers migration with seed data and assert members groups are added.
await (new TeamMembers()).up(runner);
for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); }
// Assert that the correct number of ACLs and groups were re-added.
assert.equal(await getAclRowCount(runner), initAclCount);
assert.equal(await getGroupRowCount(runner), initGroupCount);
return await disableSqliteForeignKey(async () => {
const runner = home.connection.createQueryRunner();
// Perform full up migration and add the seed data.
for (const migration of migrations) {
await (new migration()).up(runner);
}
await addSeedData(home.connection);
const initAclCount = await getAclRowCount(runner);
const initGroupCount = await getGroupRowCount(runner);

// Assert that members groups are present to start.
for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); }

// Perform down TeamMembers migration with seed data and assert members groups are removed.
await (new TeamMembers()).down(runner);
const downMigratedOrgs = await getAllOrgs(runner);
for (const org of downMigratedOrgs) { assertMembersGroup(org, false); }
// Assert that the correct number of ACLs and groups were removed.
assert.equal(await getAclRowCount(runner), initAclCount - downMigratedOrgs.length);
assert.equal(await getGroupRowCount(runner), initGroupCount - downMigratedOrgs.length);

// Perform up TeamMembers migration with seed data and assert members groups are added.
await (new TeamMembers()).up(runner);
for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); }
// Assert that the correct number of ACLs and groups were re-added.
assert.equal(await getAclRowCount(runner), initAclCount);
assert.equal(await getGroupRowCount(runner), initGroupCount);
});
});
});

Expand All @@ -223,3 +225,14 @@ async function getGroupRowCount(queryRunner: QueryRunner): Promise<number> {
const rows = await queryRunner.query(`SELECT id FROM groups`);
return rows.length;
}

// sqlite migrations need foreign keys turned off temporarily
async function disableSqliteForeignKey<T>(cb: () => Promise<T>): Promise<T> {
const sqlite = home.connection.driver.options.type === 'sqlite';
if (sqlite) { await home.connection.query("PRAGMA foreign_keys = OFF;"); }
try {
return await cb();
} finally {
if (sqlite) { await home.connection.query("PRAGMA foreign_keys = ON;"); }
}
}

0 comments on commit fbce58e

Please sign in to comment.