Skip to content

Commit

Permalink
Support POST /Groups
Browse files Browse the repository at this point in the history
  • Loading branch information
fflorent committed Jan 13, 2025
1 parent b688628 commit d341dc3
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 20 deletions.
18 changes: 14 additions & 4 deletions app/gen-server/lib/homedb/GroupsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ export class GroupsManager {
const group = Group.create({
type: groupDescriptor.type,
name: groupDescriptor.name,
memberUsers: await this._usersManager.getUsersByIds(groupDescriptor.memberUsers ?? [], manager),
memberGroups: await this._getGroupsByIds(groupDescriptor.memberGroups ?? [], manager),
memberUsers: await this._usersManager.getUsersByIdsStrict(groupDescriptor.memberUsers ?? [], manager),
memberGroups: await this._getGroupsByIdsStrict(groupDescriptor.memberGroups ?? [], manager),
});
return await manager.save(group);
});
Expand All @@ -306,8 +306,8 @@ export class GroupsManager {
id,
type: groupDescriptor.type,
name: groupDescriptor.name,
memberUsers: await this._usersManager.getUsersByIds(groupDescriptor.memberUsers ?? [], manager),
memberGroups: await this._getGroupsByIds(groupDescriptor.memberGroups ?? [], manager),
memberUsers: await this._usersManager.getUsersByIdsStrict(groupDescriptor.memberUsers ?? [], manager),
memberGroups: await this._getGroupsByIdsStrict(groupDescriptor.memberGroups ?? [], manager),
});
return await manager.save(updatedGroup);
});
Expand Down Expand Up @@ -365,6 +365,16 @@ export class GroupsManager {
});
}

private async _getGroupsByIdsStrict(groupIds: number[], optManager?: EntityManager): Promise<Group[]> {
const groups = await this._getGroupsByIds(groupIds, optManager);
if (groups.length !== groupIds.length) {
const foundGroupIds = new Set(groups.map(group => group.id));
const missingGroupIds = groupIds.filter(id => !foundGroupIds.has(id));
throw new ApiError('Groups not found: ' + missingGroupIds.join(', '), 404);
}
return groups;
}

private _getGroupsQueryBuilder(manager: EntityManager) {
return manager.createQueryBuilder()
.select('groups')
Expand Down
15 changes: 15 additions & 0 deletions app/gen-server/lib/homedb/UsersManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,21 @@ export class UsersManager {
});
}

/**
* Returns a Promise for an array of User entites for the given userIds.
* Throws an error if any of the users are not found.
* This is useful when we expect all users to exist, and want to throw an error if they don't.
*/
public async getUsersByIdsStrict(userIds: number[], optManager?: EntityManager): Promise<User[]> {
const users = await this.getUsersByIds(userIds, optManager);
if (users.length !== userIds.length) {
const foundUserIds = new Set(users.map(user => user.id));
const missingUserIds = userIds.filter(userId => !foundUserIds.has(userId));
throw new ApiError('Users not found: ' + missingUserIds.join(', '), 404);
}
return users;
}

/**
* Don't add everyone@ as a guest, unless also sharing with anon@.
* This means that material shared with everyone@ doesn't become
Expand Down
18 changes: 10 additions & 8 deletions app/server/lib/scim/v2/BaseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ import { RequestContext } from 'app/server/lib/scim/v2/ScimTypes';
import SCIMMY from "scimmy";

export class BaseController {
protected static getIdFromResource(resource: any) {
protected logger = new LogMethods(this.constructor.name, () => ({}));

constructor(
protected dbManager: HomeDBManager,
protected checkAccess: (context: RequestContext) => void,
private _invalidIdError: string
) {}

protected getIdFromResource(resource: any) {
const id = parseInt(resource.id, 10);
if (Number.isNaN(id)) {
throw new SCIMMY.Types.Error(400, 'invalidValue', 'Invalid passed group ID');
throw new SCIMMY.Types.Error(400, 'invalidValue', this._invalidIdError);
}
return id;
}

protected logger = new LogMethods(this.constructor.name, () => ({}));

constructor(
protected dbManager: HomeDBManager,
protected checkAccess: (context: RequestContext) => void
) {}

/**
* Runs the passed callback and handles any errors that might occur.
Expand Down
28 changes: 26 additions & 2 deletions app/server/lib/scim/v2/ScimGroupController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { Group } from 'app/gen-server/entity/Group';
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager';
import { BaseController } from 'app/server/lib/scim/v2/BaseController';
import { RequestContext } from 'app/server/lib/scim/v2/ScimTypes';
import { toSCIMMYGroup } from 'app/server/lib/scim/v2/ScimUtils';
import { toGroupDescriptor, toSCIMMYGroup } from 'app/server/lib/scim/v2/ScimUtils';

import SCIMMY from 'scimmy';

class ScimGroupController extends BaseController {
public constructor(
dbManager: HomeDBManager,
checkAccess: (context: RequestContext) => void
) {
super(dbManager, checkAccess, 'Invalid passed group ID');
}

/**
* Gets a single group with the passed ID.
*
Expand All @@ -15,7 +22,7 @@ class ScimGroupController extends BaseController {
*/
public async getSingleGroup(resource: any, context: RequestContext) {
return this.runAndHandleErrors(context, async () => {
const id = ScimGroupController.getIdFromResource(resource);
const id = this.getIdFromResource(resource);
const group = await this.dbManager.getGroupWithMembersById(id);
if (!group || group.type !== Group.RESOURCE_USERS_TYPE) {
throw new SCIMMY.Types.Error(404, null!, `Group with ID ${id} not found`);
Expand All @@ -38,6 +45,20 @@ class ScimGroupController extends BaseController {
return filter ? filter.match(scimmyGroup) : scimmyGroup;
});
}

/**
* Creates a new group with the passed data.
*
* @param data The data to create the group with
* @param context The request context
*/
public async createGroup(data: any, context: RequestContext) {
return this.runAndHandleErrors(context, async () => {
const groupDescriptor = toGroupDescriptor(data);
const group = await this.dbManager.createGroup(groupDescriptor);
return toSCIMMYGroup(group);
});
}
}

export const getScimGroupConfig = (
Expand All @@ -52,5 +73,8 @@ export const getScimGroupConfig = (
}
return await controller.getGroups(resource, context);
},
ingress: async (resource: any, data: any, context: RequestContext) => {
return await controller.createGroup(data, context);
},
};
};
13 changes: 10 additions & 3 deletions app/server/lib/scim/v2/ScimUserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import { toSCIMMYUser, toUserProfile } from 'app/server/lib/scim/v2/ScimUtils';
import SCIMMY from 'scimmy';

class ScimUserController extends BaseController {
public constructor(
dbManager: HomeDBManager,
checkAccess: (context: RequestContext) => void
) {
super(dbManager, checkAccess, 'Invalid passed user ID');
}

/**
* Gets a single user with the passed ID.
*
Expand All @@ -15,7 +22,7 @@ class ScimUserController extends BaseController {
*/
public async getSingleUser(resource: any, context: RequestContext) {
return this._runAndHandleErrors(context, async () => {
const id = BaseController.getIdFromResource(resource);
const id = this.getIdFromResource(resource);
const user = await this.dbManager.getUser(id);
if (!user) {
throw new SCIMMY.Types.Error(404, null!, `User with ID ${id} not found`);
Expand Down Expand Up @@ -64,7 +71,7 @@ class ScimUserController extends BaseController {
*/
public async overwriteUser(resource: any, data: any, context: RequestContext) {
return this._runAndHandleErrors(context, async () => {
const id = BaseController.getIdFromResource(resource);
const id = this.getIdFromResource(resource);
if (this.dbManager.getSpecialUserIds().includes(id)) {
throw new SCIMMY.Types.Error(403, null!, 'System user modification not permitted.');
}
Expand All @@ -82,7 +89,7 @@ class ScimUserController extends BaseController {
*/
public async deleteUser(resource: any, context: RequestContext) {
return this._runAndHandleErrors(context, async () => {
const id = BaseController.getIdFromResource(resource);
const id = this.getIdFromResource(resource);
if (this.dbManager.getSpecialUserIds().includes(id)) {
throw new SCIMMY.Types.Error(403, null!, 'System user deletion not permitted.');
}
Expand Down
29 changes: 27 additions & 2 deletions app/server/lib/scim/v2/ScimUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { User } from "app/gen-server/entity/User";
import { Group } from "app/gen-server/entity/Group";
import SCIMMY from "scimmy";
import log from 'app/server/lib/log';
import { GroupWithMembersDescriptor } from "app/gen-server/lib/homedb/Interfaces";

const SCIM_API_BASE_PATH = '/api/scim/v2';
const SCIMMY_USER_TYPE = 'User';
const SCIMMY_GROUP_TYPE = 'Group';

/**
* Converts a user from your database to a SCIMMY user
Expand Down Expand Up @@ -59,15 +62,37 @@ export function toSCIMMYGroup(group: Group) {
value: String(member.id),
display: member.name,
$ref: `${SCIM_API_BASE_PATH}/Users/${member.id}`,
type: 'User',
type: SCIMMY_USER_TYPE,
})),
// As of 2025-01-12, we don't support nested groups, so it should always be empty
...group.memberGroups.map((member: any) => ({
value: String(member.id),
display: member.name,
$ref: `${SCIM_API_BASE_PATH}/Groups/${member.id}`,
type: 'Group',
type: SCIMMY_GROUP_TYPE,
})),
],
});
}

function parseId(id: string, type: typeof SCIMMY_USER_TYPE | typeof SCIMMY_GROUP_TYPE): number {
const parsedId = parseInt(id, 10);
if (Number.isNaN(parsedId)) {
throw new SCIMMY.Types.Error(400, 'invalidValue', `Invalid ${type} member ID: ${id}`);
}
return parsedId;
}

export function toGroupDescriptor(scimGroup: any): GroupWithMembersDescriptor {
const members = scimGroup.members ?? [];
return {
name: scimGroup.displayName,
type: Group.RESOURCE_USERS_TYPE,
memberUsers: members
.filter((member: any) => member.type === SCIMMY_USER_TYPE)
.map((member: any) => parseId(member.value, SCIMMY_USER_TYPE)),
memberGroups: members
.filter((member: any) => member.type === SCIMMY_GROUP_TYPE)
.map((member: any) => parseId(member.value, SCIMMY_GROUP_TYPE)),
};
}
Loading

0 comments on commit d341dc3

Please sign in to comment.