add group endpoints

This commit is contained in:
Hugo Renard 2022-02-15 13:38:33 +01:00
parent 835c2aa53c
commit 4f1fa62c83
Signed by: hougo
GPG key ID: 3A285FD470209C59
14 changed files with 493 additions and 139 deletions

52
Context.ts Normal file
View file

@ -0,0 +1,52 @@
import {
IHttp,
IModify,
IPersistence,
IRead,
} from "@rocket.chat/apps-engine/definition/accessors";
import {
IApiEndpointInfo,
IApiRequest,
} from "@rocket.chat/apps-engine/definition/api";
import { EmptyRequestError } from "./errors/EmptyRequestError";
import { RcSdk } from "./rc-sdk/RcSdk";
export class Context {
public readonly rc: RcSdk;
public readonly request: IApiRequest;
public readonly endpoint: IApiEndpointInfo;
public readonly read: IRead;
public readonly modify: IModify;
public readonly http: IHttp;
public readonly persis: IPersistence;
constructor(
request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
) {
this.rc = new RcSdk(http, read);
this.request = request;
this.endpoint = endpoint;
this.read = read;
this.modify = modify;
this.http = http;
this.persis = persis;
}
public id(): string {
return this.request.params.id;
}
public content(): any {
if (
!this.request.content ||
Object.keys(this.request.content).length === 0
) {
throw new EmptyRequestError();
}
return this.request.content;
}
}

81
GroupEndpoint.ts Normal file
View file

@ -0,0 +1,81 @@
import { HttpStatusCode } from "@rocket.chat/apps-engine/definition/accessors";
import { IApiResponse } from "@rocket.chat/apps-engine/definition/api";
import { Context } from "./Context";
import { SCIMGroup } from "./scim/Group";
import { SCIMUser } from "./scim/User";
import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint";
export class GroupEndpoint extends ScimEndpoint implements IScimEndpoint {
public path = "Groups/:id";
public async _get(ctx: Context): Promise<IApiResponse> {
const getTeamInfo = async () => {
const teamRaw = await ctx.rc.team.info(ctx.id());
this.handleError(teamRaw);
return teamRaw.teamInfo;
};
const getTeamMembers = async () => {
const membersRaw = await ctx.rc.team.members(ctx.id());
this.handleError(membersRaw);
return membersRaw.members;
};
const teamArgs = await Promise.all([getTeamInfo(), getTeamMembers()]);
const group = SCIMGroup.fromRC(...teamArgs);
return this.success(group);
}
public async _put(ctx: Context): Promise<IApiResponse> {
const membersRaw = await ctx.rc.team.members(ctx.id());
this.handleError(membersRaw);
const targetIds = new Set<string>(
SCIMGroup.fromPlain(ctx.content()).members.map((x) => x.value),
);
const currentIds = new Set<string>(
membersRaw.members.map((x: ITeamMember) => x.user._id),
);
const removeMember = async (userId: string) => {
const removeMembersRaw = await ctx.rc.team.removeMember({
userId,
teamId: ctx.id(),
});
this.handleError(removeMembersRaw);
};
const addMembers = async (userIds: Array<string>) => {
const addMembersRaw = await ctx.rc.team.addMembers({
teamId: ctx.id(),
members: userIds.map((userId) => ({
userId,
roles: ["member"],
})),
});
this.handleError(addMembersRaw);
};
const promises: Array<Promise<void>> = [];
for (const currentId of currentIds) {
if (!targetIds.has(currentId)) {
promises.push(removeMember(currentId));
}
}
const addMemberIds: Array<string> = [];
for (const targetId of targetIds) {
if (!currentIds.has(targetId)) {
addMemberIds.push(targetId);
}
}
if (addMemberIds.length > 0) {
promises.push(addMembers(addMemberIds));
}
await Promise.all(promises);
return this._get(ctx);
}
public async _delete(ctx: Context): Promise<IApiResponse> {
const o = await ctx.rc.team.delete({ teamId: ctx.id() });
this.handleError(o);
return this.response({
status: HttpStatusCode.NO_CONTENT,
});
}
}

53
GroupsEndpoint.ts Normal file
View file

@ -0,0 +1,53 @@
import {
HttpStatusCode,
IHttp,
IModify,
IPersistence,
IRead,
} from "@rocket.chat/apps-engine/definition/accessors";
import {
IApiEndpointInfo,
IApiRequest,
IApiResponse,
} from "@rocket.chat/apps-engine/definition/api";
import crypto = require("crypto");
import { Context } from "./Context";
import { RcHttp } from "./RcHttp";
import { SCIMGroup } from "./scim/Group";
import { SCIMListResponse } from "./scim/ListResponse";
import { SCIMUser } from "./scim/User";
import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint";
export class GroupsEndpoint extends ScimEndpoint implements IScimEndpoint {
public path = "Groups";
public async _get(ctx: Context): Promise<IApiResponse> {
const teamsRaw = await ctx.rc.team.listAll();
this.handleError(teamsRaw);
const groups = teamsRaw.teams.map(async (team: ITeam) => {
const membersRaw = await ctx.rc.team.members(team._id);
this.handleError(membersRaw);
return SCIMGroup.fromRC(team, membersRaw.members);
});
const list = new SCIMListResponse();
list.Resources = await Promise.all(groups);
list.totalResults = teamsRaw.total;
return this.success(list);
}
public async _post(ctx: Context): Promise<IApiResponse> {
const u = SCIMGroup.fromPlain(ctx.content());
const o = await ctx.rc.team.create({
name: u.displayName,
type: 0,
members: u.members.map((x) => x.value),
});
this.handleError(o);
const m = await ctx.rc.team.members(o.team._id);
const group = SCIMGroup.fromRC(o.team, m.members);
return this.response({
status: HttpStatusCode.CREATED,
content: group,
});
}
}

View file

@ -18,35 +18,35 @@ export class RcHttp implements IHttp {
public async get(url: string, content?: any): Promise<IHttpResponse> { public async get(url: string, content?: any): Promise<IHttpResponse> {
return this.http.get( return this.http.get(
this.buildUrl(url), this.buildUrl(url),
await this.buildOptions(content) await this.buildOptions(content),
); );
} }
public async post(url: string, content?: any): Promise<IHttpResponse> { public async post(url: string, content?: any): Promise<IHttpResponse> {
return this.http.post( return this.http.post(
this.buildUrl(url), this.buildUrl(url),
await this.buildOptions(content) await this.buildOptions(content),
); );
} }
public async put(url: string, content?: any): Promise<IHttpResponse> { public async put(url: string, content?: any): Promise<IHttpResponse> {
return this.http.put( return this.http.put(
this.buildUrl(url), this.buildUrl(url),
await this.buildOptions(content) await this.buildOptions(content),
); );
} }
public async del(url: string, content?: any): Promise<IHttpResponse> { public async del(url: string, content?: any): Promise<IHttpResponse> {
return this.http.del( return this.http.del(
this.buildUrl(url), this.buildUrl(url),
await this.buildOptions(content) await this.buildOptions(content),
); );
} }
public async patch(url: string, content?: any): Promise<IHttpResponse> { public async patch(url: string, content?: any): Promise<IHttpResponse> {
return this.http.patch( return this.http.patch(
this.buildUrl(url), this.buildUrl(url),
await this.buildOptions(content) await this.buildOptions(content),
); );
} }

View file

@ -10,6 +10,8 @@ import {
import { App } from "@rocket.chat/apps-engine/definition/App"; import { App } from "@rocket.chat/apps-engine/definition/App";
import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata";
import { SettingType } from "@rocket.chat/apps-engine/definition/settings"; import { SettingType } from "@rocket.chat/apps-engine/definition/settings";
import { GroupEndpoint } from "./GroupEndpoint";
import { GroupsEndpoint } from "./GroupsEndpoint";
import { UserEndpoint } from "./UserEndpoint"; import { UserEndpoint } from "./UserEndpoint";
import { UsersEndpoint } from "./UsersEndpoint"; import { UsersEndpoint } from "./UsersEndpoint";
@ -23,7 +25,12 @@ export class ScimApp extends App {
configuration.api.provideApi({ configuration.api.provideApi({
visibility: ApiVisibility.PUBLIC, visibility: ApiVisibility.PUBLIC,
security: ApiSecurity.UNSECURE, security: ApiSecurity.UNSECURE,
endpoints: [new UsersEndpoint(this), new UserEndpoint(this)], endpoints: [
new UsersEndpoint(this),
new UserEndpoint(this),
new GroupsEndpoint(this),
new GroupEndpoint(this),
],
}); });
configuration.settings.provideSetting({ configuration.settings.provideSetting({

View file

@ -13,6 +13,7 @@ import {
IApiResponse, IApiResponse,
} from "@rocket.chat/apps-engine/definition/api"; } from "@rocket.chat/apps-engine/definition/api";
import { IApp } from "@rocket.chat/apps-engine/definition/IApp"; import { IApp } from "@rocket.chat/apps-engine/definition/IApp";
import { Context } from "./Context";
import { ConflictError } from "./errors/ConflictError"; import { ConflictError } from "./errors/ConflictError";
import { EmptyRequestError } from "./errors/EmptyRequestError"; import { EmptyRequestError } from "./errors/EmptyRequestError";
import { EmptyResponseError } from "./errors/EmptyResponseError"; import { EmptyResponseError } from "./errors/EmptyResponseError";
@ -29,38 +30,10 @@ type ApiEndpointMethod = (
) => Promise<IApiResponse>; ) => Promise<IApiResponse>;
export interface IScimEndpoint { export interface IScimEndpoint {
_get?( _get?(ctx: Context): Promise<IApiResponse>;
request: IApiRequest, _post?(ctx: Context): Promise<IApiResponse>;
endpoint: IApiEndpointInfo, _put?(ctx: Context): Promise<IApiResponse>;
read: IRead, _delete?(ctx: Context): Promise<IApiResponse>;
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse>;
_post?(
request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse>;
_put?(
request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse>;
_delete?(
request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse>;
} }
export abstract class ScimEndpoint extends ApiEndpoint { export abstract class ScimEndpoint extends ApiEndpoint {
@ -120,12 +93,12 @@ export abstract class ScimEndpoint extends ApiEndpoint {
protected handleError(o: any) { protected handleError(o: any) {
if (!o.success) { if (!o.success) {
if (o.error.includes("already in use")) { if (o.error?.includes("already in use")) {
throw new ConflictError( throw new ConflictError(
o.error.includes("@") ? "email" : "username", o.error.includes("@") ? "email" : "username",
); );
} }
if (o.error.includes("not found")) { if (o.error?.includes("not found")) {
} }
throw new Error(o.error); throw new Error(o.error);
} }
@ -146,12 +119,7 @@ export abstract class ScimEndpoint extends ApiEndpoint {
): Promise<IApiResponse> => { ): Promise<IApiResponse> => {
try { try {
return await method.bind(this)( return await method.bind(this)(
request, new Context(request, endpoint, read, modify, http, persis),
endpoint,
read,
modify,
http,
persis,
); );
} catch (e) { } catch (e) {
let err: SCIMError; let err: SCIMError;

View file

@ -1,75 +1,33 @@
import { import { HttpStatusCode } from "@rocket.chat/apps-engine/definition/accessors";
HttpStatusCode, import { IApiResponse } from "@rocket.chat/apps-engine/definition/api";
IHttp, import { Context } from "./Context";
IModify,
IPersistence,
IRead,
} from "@rocket.chat/apps-engine/definition/accessors";
import {
IApiEndpointInfo,
IApiRequest,
IApiResponse,
} from "@rocket.chat/apps-engine/definition/api";
import { RcHttp } from "./RcHttp";
import { SCIMUser } from "./scim/User"; import { SCIMUser } from "./scim/User";
import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint"; import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint";
export class UserEndpoint extends ScimEndpoint implements IScimEndpoint { export class UserEndpoint extends ScimEndpoint implements IScimEndpoint {
public path = "Users/:id"; public path = "Users/:id";
public async _get( public async _get(ctx: Context): Promise<IApiResponse> {
request: IApiRequest, const o = await ctx.rc.user.info(ctx.id());
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse> {
const response = await new RcHttp(http, read).get(
`users.info?userId=${request.params.id}`,
);
const o = this.parseResponse(response);
this.handleError(o); this.handleError(o);
const user = SCIMUser.fromRC(o.user); const user = SCIMUser.fromRC(o.user);
return this.success(user); return this.success(user);
} }
public async _put( public async _put(ctx: Context): Promise<IApiResponse> {
request: IApiRequest, const o = await ctx.rc.user.update(
endpoint: IApiEndpointInfo, this.scimToUserUpdate(ctx.id(), SCIMUser.fromPlain(ctx.content())),
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse> {
this.hasContent(request);
const response = await new RcHttp(http, read).post(
"users.update",
this.scimToUserUpdate(
request.params.id,
SCIMUser.fromPlain(request.content),
),
); );
const o = this.parseResponse(response);
this.handleError(o); this.handleError(o);
const user = SCIMUser.fromRC(o.user); const user = SCIMUser.fromRC(o.user);
return this.success(user); return this.success(user);
} }
public async _delete( public async _delete(ctx: Context): Promise<IApiResponse> {
request: IApiRequest, const o = await ctx.rc.user.delete({
endpoint: IApiEndpointInfo, userId: ctx.id(),
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse> {
const d: IUserDelete = {
userId: request.params.id,
confirmRelinquish: true, confirmRelinquish: true,
}; });
const response = await new RcHttp(http, read).post("users.delete", d);
const o = this.parseResponse(response);
this.handleError(o); this.handleError(o);
return this.response({ return this.response({
status: HttpStatusCode.NO_CONTENT, status: HttpStatusCode.NO_CONTENT,

View file

@ -1,17 +1,7 @@
import { import { HttpStatusCode } from "@rocket.chat/apps-engine/definition/accessors";
HttpStatusCode, import { IApiResponse } from "@rocket.chat/apps-engine/definition/api";
IHttp,
IModify,
IPersistence,
IRead,
} from "@rocket.chat/apps-engine/definition/accessors";
import {
IApiEndpointInfo,
IApiRequest,
IApiResponse,
} from "@rocket.chat/apps-engine/definition/api";
import crypto = require("crypto"); import crypto = require("crypto");
import { RcHttp } from "./RcHttp"; import { Context } from "./Context";
import { SCIMListResponse } from "./scim/ListResponse"; import { SCIMListResponse } from "./scim/ListResponse";
import { SCIMUser } from "./scim/User"; import { SCIMUser } from "./scim/User";
import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint"; import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint";
@ -19,18 +9,8 @@ import { IScimEndpoint, ScimEndpoint } from "./ScimEndpoint";
export class UsersEndpoint extends ScimEndpoint implements IScimEndpoint { export class UsersEndpoint extends ScimEndpoint implements IScimEndpoint {
public path = "Users"; public path = "Users";
public async _get( public async _get(ctx: Context): Promise<IApiResponse> {
request: IApiRequest, const o = await ctx.rc.user.list();
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse> {
const response = await new RcHttp(http, read).get(
`users.list?query={"type":{"$eq":"user"}}&fields={"createdAt":1}`,
);
const o = this.parseResponse(response);
this.handleError(o); this.handleError(o);
const list = new SCIMListResponse(); const list = new SCIMListResponse();
list.Resources = o.users.map(SCIMUser.fromRC); list.Resources = o.users.map(SCIMUser.fromRC);
@ -38,20 +18,10 @@ export class UsersEndpoint extends ScimEndpoint implements IScimEndpoint {
return this.success(list); return this.success(list);
} }
public async _post( public async _post(ctx: Context): Promise<IApiResponse> {
request: IApiRequest, const o = await ctx.rc.user.create(
endpoint: IApiEndpointInfo, this.scimToUserCreate(SCIMUser.fromPlain(ctx.content())),
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence,
): Promise<IApiResponse> {
this.hasContent(request);
const response = await new RcHttp(http, read).post(
`users.create`,
this.scimToUserCreate(SCIMUser.fromPlain(request.content)),
); );
const o = this.parseResponse(response);
this.handleError(o); this.handleError(o);
const user = SCIMUser.fromRC(o.user); const user = SCIMUser.fromRC(o.user);
return this.response({ return this.response({

96
rc-sdk/RcSdk.ts Normal file
View file

@ -0,0 +1,96 @@
import {
IHttp,
IHttpRequest,
IHttpResponse,
IRead,
} from "@rocket.chat/apps-engine/definition/accessors";
import { EmptyResponseError } from "../errors/EmptyResponseError";
import { JsonParseError } from "../errors/JsonParseError";
import { RcSdkTeam } from "./RcSdkTeam";
import { RcSdkUser } from "./RcSdkUser";
export class RcSdk {
public user: RcSdkUser;
public team: RcSdkTeam;
private readonly baseUrl = "http://localhost:3000/api/v1";
private readonly http: IHttp;
private readonly read: IRead;
constructor(http: IHttp, read: IRead) {
this.http = http;
this.read = read;
this.user = new RcSdkUser(this);
this.team = new RcSdkTeam(this);
}
public async get(url: string, content?: any): Promise<IHttpResponse> {
return this.http.get(
this.buildUrl(url),
await this.buildOptions(content),
);
}
public async post(url: string, content?: any): Promise<IHttpResponse> {
return this.http.post(
this.buildUrl(url),
await this.buildOptions(content),
);
}
public async put(url: string, content?: any): Promise<IHttpResponse> {
return this.http.put(
this.buildUrl(url),
await this.buildOptions(content),
);
}
public async del(url: string, content?: any): Promise<IHttpResponse> {
return this.http.del(
this.buildUrl(url),
await this.buildOptions(content),
);
}
public async patch(url: string, content?: any): Promise<IHttpResponse> {
return this.http.patch(
this.buildUrl(url),
await this.buildOptions(content),
);
}
public parseResponse(response: IHttpResponse): any {
if (!response.content) {
throw new EmptyResponseError();
}
let content: any;
try {
content = JSON.parse(response.content);
} catch (e) {
throw new JsonParseError();
}
return content;
}
private buildUrl(url: string): string {
return `${this.baseUrl}/${url}`;
}
private async buildOptions(content?: any): Promise<IHttpRequest> {
const options: IHttpRequest = {
headers: {
"X-User-Id": await this.read
.getEnvironmentReader()
.getSettings()
.getValueById("rc-user-id"),
"X-Auth-Token": await this.read
.getEnvironmentReader()
.getSettings()
.getValueById("rc-token"),
"Content-Type": "application/json",
},
};
if (content !== undefined) {
options.content = JSON.stringify(content);
}
return options;
}
}

70
rc-sdk/RcSdkTeam.ts Normal file
View file

@ -0,0 +1,70 @@
import { RcSdk } from "./RcSdk";
interface ITeamRemoveMemberBody {
teamId: string;
userId: string;
}
interface ITeamAddMemberBody {
teamId: string;
members: Array<{
userId: string;
roles: Array<string>;
}>;
}
interface ITeamDeleteBody {
teamId: string;
roomsToRemove?: Array<string>;
}
interface ITeamCreateBody {
name: string;
type: 0 | 1;
members?: Array<string>;
room?: {
readOnly: boolean;
};
}
export class RcSdkTeam {
private sdk: RcSdk;
constructor(sdk: RcSdk) {
this.sdk = sdk;
}
public async listAll(): Promise<any> {
const response = await this.sdk.get(`teams.listAll`);
return this.sdk.parseResponse(response);
}
public async members(teamId: string): Promise<any> {
const response = await this.sdk.get(`teams.members?teamId=${teamId}`);
return this.sdk.parseResponse(response);
}
public async info(teamId: string): Promise<any> {
const response = await this.sdk.get(`teams.info?teamId=${teamId}`);
return this.sdk.parseResponse(response);
}
public async delete(body: ITeamDeleteBody): Promise<any> {
const response = await this.sdk.post(`teams.delete`, body);
return this.sdk.parseResponse(response);
}
public async create(body: ITeamCreateBody): Promise<any> {
const response = await this.sdk.post(`teams.create`, body);
return this.sdk.parseResponse(response);
}
public async removeMember(body: ITeamRemoveMemberBody): Promise<any> {
const response = await this.sdk.post(`teams.removeMember`, body);
return this.sdk.parseResponse(response);
}
public async addMembers(body: ITeamAddMemberBody): Promise<any> {
const response = await this.sdk.post(`teams.addMembers`, body);
return this.sdk.parseResponse(response);
}
}

35
rc-sdk/RcSdkUser.ts Normal file
View file

@ -0,0 +1,35 @@
import { RcSdk } from "./RcSdk";
export class RcSdkUser {
private sdk: RcSdk;
constructor(sdk: RcSdk) {
this.sdk = sdk;
}
public async list(): Promise<any> {
const response = await this.sdk.get(
`users.list?query={"type":{"$eq":"user"}}&fields={"createdAt":1}`,
);
return this.sdk.parseResponse(response);
}
public async info(userId: string): Promise<any> {
const response = await this.sdk.get(`users.info?userId=${userId}`);
return this.sdk.parseResponse(response);
}
public async update(body: IUserUpdate): Promise<any> {
const response = await this.sdk.post(`users.update`, body);
return this.sdk.parseResponse(response);
}
public async delete(body: IUserDelete): Promise<any> {
const response = await this.sdk.post(`users.delete`);
return this.sdk.parseResponse(response);
}
public async create(body: IUserCreate): Promise<any> {
const response = await this.sdk.post(`users.create`, body);
return this.sdk.parseResponse(response);
}
}

18
rest.ts
View file

@ -41,4 +41,22 @@ interface IUser {
username: string; username: string;
active?: boolean; active?: boolean;
createdAt: string; createdAt: string;
_updatedAt?: string;
}
interface ITeam {
_id: string;
name: string;
active?: boolean;
createdAt: string;
_updatedAt?: string;
}
interface ITeamMember {
user: {
_id: string;
name: string;
username: string;
};
createdAt: string;
} }

40
scim/Group.ts Normal file
View file

@ -0,0 +1,40 @@
import { ISCIMGroupMember, ISCIMResource } from "./Interfaces";
import { SCIMMeta } from "./Meta";
export class SCIMGroup implements ISCIMResource {
public static fromPlain(plain: SCIMGroup): SCIMGroup {
const group = new SCIMGroup();
group.id = plain.id;
group.externalId = plain.externalId;
group.displayName = plain.displayName;
group.members = plain.members || [];
return group;
}
public static fromRC(rc: ITeam, members: Array<ITeamMember>): SCIMGroup {
const group = new SCIMGroup();
group.id = rc._id;
group.externalId = rc._id;
group.displayName = rc.name;
group.meta.created = new Date(rc.createdAt);
group.meta.lastModified = new Date(rc._updatedAt || rc.createdAt);
group.members = members.map((member) => ({
value: member.user._id,
$ref: `/Users/${member.user._id}`,
display: member.user.name,
type: "User",
}));
return group;
}
public readonly schemas = ["urn:ietf:params:scim:schemas:core:2.0:Group"];
public meta: SCIMMeta;
public id: string;
public externalId: string;
public displayName: string;
public members: Array<ISCIMGroupMember> = [];
constructor() {
this.meta = new SCIMMeta("Group", () => this.id);
}
}

View file

@ -26,7 +26,13 @@ export interface ISCIMListResponse {
{ {
id: "c75ad752-64ae-4823-840d-ffa80929976c"; id: "c75ad752-64ae-4823-840d-ffa80929976c";
userName: "jsmith"; userName: "jsmith";
} },
]; ];
} }
export interface ISCIMGroupMember {
value: string;
$ref: string;
display: string;
type?: "User" | "Group";
}