From 65bf284856b1171423e8e7fcc85e4019c77cfc58 Mon Sep 17 00:00:00 2001 From: Hugo Renard Date: Fri, 11 Feb 2022 14:13:06 +0100 Subject: [PATCH] minimal working --- UserEndpoint.ts | 130 +++++++++---------------- UsersEndpoint.ts | 225 ++++++++++++++++++------------------------- rest.ts | 1 + scim.ts | 16 --- scim/Error.ts | 41 ++++++++ scim/Interfaces.ts | 32 ++++++ scim/ListResponse.ts | 11 +++ scim/Meta.ts | 28 ++++++ scim/User.ts | 56 +++++++++++ 9 files changed, 306 insertions(+), 234 deletions(-) delete mode 100644 scim.ts create mode 100644 scim/Error.ts create mode 100644 scim/Interfaces.ts create mode 100644 scim/ListResponse.ts create mode 100644 scim/Meta.ts create mode 100644 scim/User.ts diff --git a/UserEndpoint.ts b/UserEndpoint.ts index f45bd2f..4b112b9 100644 --- a/UserEndpoint.ts +++ b/UserEndpoint.ts @@ -11,84 +11,10 @@ import { IApiRequest, IApiResponse, } from "@rocket.chat/apps-engine/definition/api"; -const crypto = require("crypto"); +import { SCIMUser } from "./scim/User"; export class UserEndpoint extends ApiEndpoint { public path = "Users/:id"; - // private http: IHttp; - // private authHeaders: { [key: string]: string }; - - // constructor(app: IApp) { - // super(app); - // } - - // private async updateState(http: IHttp, read: IRead) { - // this.http = http; - // this.authHeaders = { - // "X-User-Id": await read - // .getEnvironmentReader() - // .getSettings() - // .getValueById("rc-user-id"), - // "X-Auth-Token": await read - // .getEnvironmentReader() - // .getSettings() - // .getValueById("rc-token"), - // }; - // } - - private async getAuthHeaders( - read: IRead - ): Promise<{ [key: string]: string }> { - return { - "X-User-Id": await read - .getEnvironmentReader() - .getSettings() - .getValueById("rc-user-id"), - "X-Auth-Token": await read - .getEnvironmentReader() - .getSettings() - .getValueById("rc-token"), - }; - } - - // private scimToRC(user: ISCIMUser): IUser { - // return { - // email: user.emails[0].value, - // name: user.displayName, - // password: "P@ssw0rd", - // username: user.userName, - // verified: true, - // }; - // } - - private scimToUserUpdate(userId: string, user: ISCIMUser): IUserUpdate { - return { - userId, - data: { - email: user.emails[0].value, - name: user.displayName, - username: user.userName, - active: user.active, - verified: true, - customFields: { - scimExternalId: user.externalId, - }, - }, - }; - } - - private rcToSCIM(user: IUser): ISCIMUser { - return { - id: user._id, - externalId: "", - emails: [ - { type: "work", primary: true, value: user.emails[0].address }, - ], - displayName: user.name, - userName: user.username, - active: user.active === undefined ? true : user.active, - }; - } public async get( request: IApiRequest, @@ -98,7 +24,7 @@ export class UserEndpoint extends ApiEndpoint { http: IHttp, persis: IPersistence ): Promise { - let user: ISCIMUser; + let user: SCIMUser; try { const response = await http.get( `http://localhost:3000/api/v1/users.info?userId=` + @@ -113,12 +39,11 @@ export class UserEndpoint extends ApiEndpoint { if (!response.content) throw new Error("Empty response"); const o = JSON.parse(response.content); if (!o.success) throw new Error(o.error); - // user = this.rcToSCIM(o.user); - user = this.rcToSCIM(o.user); + user = SCIMUser.fromRC(o.user); } catch (e) { return { headers: { - "Content-Type": "application/json+scim", + "Content-Type": "application/scim+json", }, status: HttpStatusCode.BAD_REQUEST, content: { message: e.message }, @@ -126,7 +51,7 @@ export class UserEndpoint extends ApiEndpoint { } return { headers: { - "Content-Type": "application/json+scim", + "Content-Type": "application/scim+json", }, status: HttpStatusCode.FOUND, content: user, @@ -141,7 +66,7 @@ export class UserEndpoint extends ApiEndpoint { http: IHttp, persis: IPersistence ): Promise { - let user: ISCIMUser; + let user: SCIMUser; try { const response = await http.post( "http://localhost:3000/api/v1/users.update", @@ -153,7 +78,7 @@ export class UserEndpoint extends ApiEndpoint { content: JSON.stringify( this.scimToUserUpdate( request.params.id, - request.content + SCIMUser.fromPlain(request.content) ) ), } @@ -161,11 +86,11 @@ export class UserEndpoint extends ApiEndpoint { if (!response.content) throw new Error("Empty response"); const o = JSON.parse(response.content); if (!o.success) throw new Error(o.error); - user = this.rcToSCIM(o.user); + user = SCIMUser.fromRC(o.user); } catch (e) { return { headers: { - "Content-Type": "application/json+scim", + "Content-Type": "application/scim+json", }, status: HttpStatusCode.BAD_REQUEST, content: { message: e.message }, @@ -173,7 +98,7 @@ export class UserEndpoint extends ApiEndpoint { } return { headers: { - "Content-Type": "application/json+scim", + "Content-Type": "application/scim+json", }, status: HttpStatusCode.FOUND, content: user, @@ -209,7 +134,7 @@ export class UserEndpoint extends ApiEndpoint { } catch (e) { return { headers: { - "Content-Type": "application/json+scim", + "Content-Type": "application/scim+json", }, status: HttpStatusCode.BAD_REQUEST, content: { message: e.message }, @@ -217,9 +142,40 @@ export class UserEndpoint extends ApiEndpoint { } return { headers: { - "Content-Type": "application/json+scim", + "Content-Type": "application/scim+json", }, status: HttpStatusCode.NO_CONTENT, }; } + + private async getAuthHeaders( + read: IRead + ): Promise<{ [key: string]: string }> { + return { + "X-User-Id": await read + .getEnvironmentReader() + .getSettings() + .getValueById("rc-user-id"), + "X-Auth-Token": await read + .getEnvironmentReader() + .getSettings() + .getValueById("rc-token"), + }; + } + + private scimToUserUpdate(userId: string, user: SCIMUser): IUserUpdate { + return { + userId, + data: { + email: user.getEmail(), + name: user.displayName, + username: user.userName, + active: user.active, + verified: true, + customFields: { + scimExternalId: user.externalId, + }, + }, + }; + } } diff --git a/UsersEndpoint.ts b/UsersEndpoint.ts index 646924e..c374cbb 100644 --- a/UsersEndpoint.ts +++ b/UsersEndpoint.ts @@ -11,30 +11,103 @@ import { IApiRequest, IApiResponse, } from "@rocket.chat/apps-engine/definition/api"; -const crypto = require("crypto"); +import crypto = require("crypto"); +import { SCIMError, SCIMErrorType } from "./scim/Error"; +import { SCIMListResponse } from "./scim/ListResponse"; +import { SCIMUser } from "./scim/User"; export class UsersEndpoint extends ApiEndpoint { public path = "Users"; - // private http: IHttp; - // private authHeaders: { [key: string]: string }; - // constructor(app: IApp) { - // super(app); - // } + public async get( + request: IApiRequest, + endpoint: IApiEndpointInfo, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise { + const list = new SCIMListResponse(); + try { + const response = await http.get( + `http://localhost:3000/api/v1/users.list?query={"type":{"$eq":"user"}}&fields={"createdAt":1}`, + { + headers: { + ...(await this.getAuthHeaders(read)), + "Content-Type": "application/json", + }, + } + ); + if (!response.content) { + throw new Error("Empty response"); + } + const o = JSON.parse(response.content); + if (!o.success) { + throw new Error(o.error); + } + list.Resources = o.users.map(SCIMUser.fromRC); + list.totalResults = o.total; + } catch (e) { + const err = new SCIMError(); + err.scimType = SCIMErrorType.INVALID_VALUE; + err.detail = e.message; + err.status = "400"; + return err.toApiResponse(); + } + return { + headers: { + "Content-Type": "application/scim+json", + }, + status: HttpStatusCode.FOUND, + content: list, + }; + } - // private async updateState(http: IHttp, read: IRead) { - // this.http = http; - // this.authHeaders = { - // "X-User-Id": await read - // .getEnvironmentReader() - // .getSettings() - // .getValueById("rc-user-id"), - // "X-Auth-Token": await read - // .getEnvironmentReader() - // .getSettings() - // .getValueById("rc-token"), - // }; - // } + public async post( + request: IApiRequest, + endpoint: IApiEndpointInfo, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise { + let user = request.content; + try { + const response = await http.post( + `http://localhost:3000/api/v1/users.create`, + { + headers: { + ...(await this.getAuthHeaders(read)), + "Content-Type": "application/json", + }, + content: JSON.stringify( + this.scimToUserCreate(SCIMUser.fromPlain(user)) + ), + } + ); + if (!response.content) { + throw new Error("Empty response"); + } + const o = JSON.parse(response.content); + if (!o.success) { + throw new Error(o.error); + } + user = SCIMUser.fromRC(o.user); + } catch (e) { + const err = new SCIMError(); + err.scimType = SCIMErrorType.INVALID_VALUE; + err.detail = e.message; + err.status = "400"; + return err.toApiResponse(); + } + return { + headers: { + "Content-Type": "application/scim+json", + }, + status: HttpStatusCode.CREATED, + content: user, + }; + } private async getAuthHeaders( read: IRead @@ -51,17 +124,7 @@ export class UsersEndpoint extends ApiEndpoint { }; } - // private scimToRC(user: ISCIMUser): IUser { - // return { - // email: user.emails[0].value, - // name: user.displayName, - // password: "P@ssw0rd", - // username: user.userName, - // verified: true, - // }; - // } - - private scimToUserCreate(user: ISCIMUser): IUserCreate { + private scimToUserCreate(user: SCIMUser): IUserCreate { return { email: user.emails[0].value, name: user.displayName, @@ -69,108 +132,8 @@ export class UsersEndpoint extends ApiEndpoint { password: crypto.randomBytes(64).toString("base64").slice(0, 64), verified: true, customFields: { - scimExternalId: user.externalId - } - }; - } - - private rcToSCIM(user: IUser): ISCIMUser { - return { - id: user._id, - externalId: "", - emails: [ - { type: "work", primary: true, value: user.emails[0].address }, - ], - displayName: user.name, - userName: user.username, - active: user.active === undefined ? true : user.active, - }; - } - - public async get( - request: IApiRequest, - endpoint: IApiEndpointInfo, - read: IRead, - modify: IModify, - http: IHttp, - persis: IPersistence - ): Promise { - let users: Array = []; - try { - const response = await http.get( - `http://localhost:3000/api/v1/users.list?query={"type": {"$eq": "user"}}`, - { - headers: { - ...(await this.getAuthHeaders(read)), - "Content-Type": "application/json", - }, - } - ); - if (!response.content) throw new Error("Empty response"); - const o = JSON.parse(response.content); - if (!o.success) throw new Error(o.error); - // user = this.rcToSCIM(o.user); - users = o.users.map(this.rcToSCIM); - } catch (e) { - return { - headers: { - "Content-Type": "application/json+scim", - }, - status: HttpStatusCode.BAD_REQUEST, - content: { message: e.message }, - }; - } - return { - headers: { - "Content-Type": "application/json+scim", + scimExternalId: user.externalId, }, - status: HttpStatusCode.FOUND, - content: users, - }; - } - - public async post( - request: IApiRequest, - endpoint: IApiEndpointInfo, - read: IRead, - modify: IModify, - http: IHttp, - persis: IPersistence - ): Promise { - let user = request.content; - try { - const response = await http.post( - "http://localhost:3000/api/v1/users.create", - { - headers: { - ...(await this.getAuthHeaders(read)), - "Content-Type": "application/json", - }, - content: JSON.stringify( - this.scimToUserCreate(request.content) - ), - } - ); - if (!response.content) throw new Error("Empty response"); - const o = JSON.parse(response.content); - if (!o.success) throw new Error(o.error); - // user = this.rcToSCIM(o.user); - user = this.rcToSCIM(o.user); - } catch (e) { - return { - headers: { - "Content-Type": "application/json+scim", - }, - status: HttpStatusCode.BAD_REQUEST, - content: { message: e.message }, - }; - } - return { - headers: { - "Content-Type": "application/json+scim", - }, - status: HttpStatusCode.CREATED, - content: user, }; } } diff --git a/rest.ts b/rest.ts index c18c9db..23de675 100644 --- a/rest.ts +++ b/rest.ts @@ -40,4 +40,5 @@ interface IUser { name: string; username: string; active?: boolean; + createdAt: string; } diff --git a/scim.ts b/scim.ts deleted file mode 100644 index 7dd3c48..0000000 --- a/scim.ts +++ /dev/null @@ -1,16 +0,0 @@ -interface ISCIMUser { - id: string; - externalId: string; - userName: string; - name?: { - familyName: string; - givenName: string; - }; - displayName: string; - active: boolean; - emails: Array<{ - value: string; - type: string; - primary: boolean; - }>; -} \ No newline at end of file diff --git a/scim/Error.ts b/scim/Error.ts new file mode 100644 index 0000000..283a892 --- /dev/null +++ b/scim/Error.ts @@ -0,0 +1,41 @@ +import { IApiResponse } from "@rocket.chat/apps-engine/definition/api"; + +export enum SCIMErrorType { + INVALID_FILTER = "invalidFilter", + TOO_MANY = "tooMany", + UNIQUENESS = "uniqueness", + MUTABILITY = "mutability", + INVALID_SYNTAX = "invalidSyntax", + INVALID_PATH = "invalidPath", + NO_TARGET = "noTarget", + INVALID_VALUE = "invalidValue", + INVALID_VERS = "invalidVers", + SENSITIVE = "sensitive", +} + +export enum SCIMErrorDetail { + TEMPORARY_REDIRECT = "The client is directed to repeat the same HTTP request at the location identified.", + PERMANENT_REDIRECT = "The client is directed to repeat the same HTTP request at the location identified.", + BAD_REQUEST = "Request is unparsable, syntactically incorrect, or violates schema.", + UNAUTHORIZED = "Authorization failure. The authorization header is invalid or missing.", + FORBIDDEN = "Operation is not permitted based on the supplied authorization.", + NOT_FOUND = "Specified resource or endpoint does not exist.", + CONFLICT = "The specified version number does not match the resource's latest version number, or a service provider refused to create a new, duplicate resource.", + PRECONDITION_FAILED = "Failed to update. Resource has changed on the server.", + PAYLOAD_TOO_LARGE = `{"maxOperations": 1000,"maxPayloadSize": 1048576}`, + INTERNAL_SERVER_ERROR = "An internal error.", + NOT_IMPLEMENTED = "Service provider does not support the request operation.", +} + +export class SCIMError { + public readonly schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"]; + public scimType: SCIMErrorType; + public detail: SCIMErrorDetail | string; + public status: string; + public toApiResponse(): IApiResponse { + return { + status: parseInt(this.status, 10), + content: this, + }; + } +} \ No newline at end of file diff --git a/scim/Interfaces.ts b/scim/Interfaces.ts new file mode 100644 index 0000000..c9f4108 --- /dev/null +++ b/scim/Interfaces.ts @@ -0,0 +1,32 @@ +import { SCIMMeta } from "./Meta"; + +export interface ISCIMResource { + id: string; + externalId: string; + meta: SCIMMeta; + schemas: Array; +} + +export interface ISCIMUserName { + familyName?: string; + givenName?: string; +} +export interface ISCIMUserEmail { + value: string; + primary: boolean; +} +export interface ISCIMListResponse { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"]; + totalResults: 2; + Resources: [ + { + id: "2819c223-7f76-453a-919d-413861904646"; + userName: "bjensen"; + }, + { + id: "c75ad752-64ae-4823-840d-ffa80929976c"; + userName: "jsmith"; + } + ]; +} + diff --git a/scim/ListResponse.ts b/scim/ListResponse.ts new file mode 100644 index 0000000..2bb27fb --- /dev/null +++ b/scim/ListResponse.ts @@ -0,0 +1,11 @@ +import { ISCIMResource } from "./Interfaces"; + +export class SCIMListResponse { + public readonly schemas = [ + "urn:ietf:params:scim:api:messages:2.0:ListResponse", + ]; + public totalResults: number; + public startIndex: number = 1; + public itemsPerPage: number; + public Resources: Array; +} diff --git a/scim/Meta.ts b/scim/Meta.ts new file mode 100644 index 0000000..aacb8b8 --- /dev/null +++ b/scim/Meta.ts @@ -0,0 +1,28 @@ +type ValueGetter = () => string; + +export class SCIMMeta { + public resourceType: string; + public created: Date; + public lastModified: Date; + public version: string; + private id: ValueGetter; + + constructor(resourceType: string, id: ValueGetter) { + this.id = id; + this.resourceType = resourceType; + const now = Date.now(); + this.created = new Date(now); + this.lastModified = new Date(now); + } + + public get location(): string { + return `/${this.resourceType}s/${this.id()}`; + } + + public toJSON() { + return { + ...this, + location: this.location, + }; + } +} diff --git a/scim/User.ts b/scim/User.ts new file mode 100644 index 0000000..057cf25 --- /dev/null +++ b/scim/User.ts @@ -0,0 +1,56 @@ +import { ISCIMResource, ISCIMUserEmail, ISCIMUserName } from "./Interfaces"; +import { SCIMMeta } from "./Meta"; + +export class SCIMUser implements ISCIMResource { + public static fromPlain(plain: SCIMUser): SCIMUser { + const user = new SCIMUser(); + user.id = plain.id; + user.externalId = plain.externalId; + user.userName = plain.userName; + user.displayName = plain.displayName; + user.active = plain.active; + user.name = plain.name; + user.emails = plain.emails; + return user; + } + + public static fromRC(rc: IUser): SCIMUser { + const user = new SCIMUser(); + user.id = rc._id; + user.externalId = rc._id; + user.setEmail(rc.emails[0].address); + user.displayName = rc.name; + user.userName = rc.username; + user.meta.created = new Date(rc.createdAt); + user.meta.lastModified = new Date(rc.createdAt); + if (user.active !== undefined) { + user.active = user.active; + } + return user; + } + + public readonly schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"]; + public meta: SCIMMeta; + public id: string; + public externalId: string; + public userName: string; + public displayName: string; + public active: boolean; + public name: ISCIMUserName = {}; + public emails: Array = []; + + constructor() { + this.meta = new SCIMMeta("User", () => this.id); + } + + public getEmail(): string { + if (this.emails.length > 0) { + return this.emails[0].value; + } + return ""; + } + + public setEmail(email: string) { + this.emails = [{ primary: true, value: email }]; + } +}