minimal working

This commit is contained in:
Hugo Renard 2022-02-11 14:13:06 +01:00
parent 4e36015873
commit 65bf284856
Signed by: hougo
GPG key ID: 3A285FD470209C59
9 changed files with 306 additions and 234 deletions

View file

@ -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<IApiResponse> {
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<IApiResponse> {
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,
},
},
};
}
}

View file

@ -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<IApiResponse> {
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<IApiResponse> {
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<IApiResponse> {
let users: Array<ISCIMUser> = [];
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",
scimExternalId: user.externalId,
},
}
);
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",
},
status: HttpStatusCode.FOUND,
content: users,
};
}
public async post(
request: IApiRequest,
endpoint: IApiEndpointInfo,
read: IRead,
modify: IModify,
http: IHttp,
persis: IPersistence
): Promise<IApiResponse> {
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,
};
}
}

View file

@ -40,4 +40,5 @@ interface IUser {
name: string;
username: string;
active?: boolean;
createdAt: string;
}

16
scim.ts
View file

@ -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;
}>;
}

41
scim/Error.ts Normal file
View file

@ -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,
};
}
}

32
scim/Interfaces.ts Normal file
View file

@ -0,0 +1,32 @@
import { SCIMMeta } from "./Meta";
export interface ISCIMResource {
id: string;
externalId: string;
meta: SCIMMeta;
schemas: Array<string>;
}
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";
}
];
}

11
scim/ListResponse.ts Normal file
View file

@ -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<ISCIMResource>;
}

28
scim/Meta.ts Normal file
View file

@ -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,
};
}
}

56
scim/User.ts Normal file
View file

@ -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<ISCIMUserEmail> = [];
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 }];
}
}