move to basic auth
This commit is contained in:
parent
4cc01ed771
commit
0b1a83841d
8 changed files with 39 additions and 85 deletions
44
ScimApp.ts
44
ScimApp.ts
|
@ -9,11 +9,9 @@ import {
|
||||||
} from "@rocket.chat/apps-engine/definition/api";
|
} from "@rocket.chat/apps-engine/definition/api";
|
||||||
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 { GroupEndpoint } from "./src/endpoints/GroupEndpoint";
|
import { GroupEndpoint } from "./src/endpoints/GroupEndpoint";
|
||||||
import { GroupsEndpoint } from "./src/endpoints/GroupsEndpoint";
|
import { GroupsEndpoint } from "./src/endpoints/GroupsEndpoint";
|
||||||
|
|
||||||
import crypto = require("crypto");
|
|
||||||
import { UserEndpoint } from "./src/endpoints/UserEndpoint";
|
import { UserEndpoint } from "./src/endpoints/UserEndpoint";
|
||||||
import { UsersEndpoint } from "./src/endpoints/UsersEndpoint";
|
import { UsersEndpoint } from "./src/endpoints/UsersEndpoint";
|
||||||
|
|
||||||
|
@ -33,47 +31,5 @@ export class ScimApp extends App {
|
||||||
new GroupEndpoint(this),
|
new GroupEndpoint(this),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
configuration.settings.provideSetting({
|
|
||||||
id: "rc-user-id",
|
|
||||||
type: SettingType.STRING,
|
|
||||||
packageValue: "",
|
|
||||||
required: true,
|
|
||||||
public: false,
|
|
||||||
i18nLabel: "Rocket.Chat User ID",
|
|
||||||
});
|
|
||||||
|
|
||||||
configuration.settings.provideSetting({
|
|
||||||
id: "rc-token",
|
|
||||||
type: SettingType.STRING,
|
|
||||||
packageValue: "",
|
|
||||||
required: true,
|
|
||||||
public: false,
|
|
||||||
i18nLabel: "Rocket.Chat Token",
|
|
||||||
});
|
|
||||||
|
|
||||||
configuration.settings.provideSetting({
|
|
||||||
id: "auth-mode",
|
|
||||||
type: SettingType.SELECT,
|
|
||||||
packageValue: "",
|
|
||||||
required: true,
|
|
||||||
public: false,
|
|
||||||
i18nLabel: "Auth mode for the SCIM endpoints.",
|
|
||||||
value: "bearer",
|
|
||||||
values: [
|
|
||||||
{ key: "bearer", i18nLabel: "Bearer token" },
|
|
||||||
{ key: "none", i18nLabel: "None" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
configuration.settings.provideSetting({
|
|
||||||
id: "auth-bearer",
|
|
||||||
type: SettingType.STRING,
|
|
||||||
packageValue: "",
|
|
||||||
required: true,
|
|
||||||
public: false,
|
|
||||||
i18nLabel: "A bearer token to access the SCIM endpoints.",
|
|
||||||
value: crypto.randomBytes(128).toString("base64").slice(0, 128),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
IApiEndpointInfo,
|
IApiEndpointInfo,
|
||||||
IApiRequest,
|
IApiRequest,
|
||||||
} from "@rocket.chat/apps-engine/definition/api";
|
} from "@rocket.chat/apps-engine/definition/api";
|
||||||
|
import { Buffer } from "buffer";
|
||||||
import { EmptyRequestError } from "../errors/EmptyRequestError";
|
import { EmptyRequestError } from "../errors/EmptyRequestError";
|
||||||
import { UnauthorizedError } from "../errors/UnauthorizedError";
|
import { UnauthorizedError } from "../errors/UnauthorizedError";
|
||||||
import { RcSdk } from "../rc-sdk/RcSdk";
|
import { RcSdk } from "../rc-sdk/RcSdk";
|
||||||
|
@ -33,8 +34,6 @@ export class Context {
|
||||||
persis: IPersistence,
|
persis: IPersistence,
|
||||||
log: ILogger,
|
log: ILogger,
|
||||||
) {
|
) {
|
||||||
this.rc = new RcSdk(http, read, log);
|
|
||||||
this.store = new Store(read, persis);
|
|
||||||
this.request = request;
|
this.request = request;
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
this.read = read;
|
this.read = read;
|
||||||
|
@ -42,6 +41,19 @@ export class Context {
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.persis = persis;
|
this.persis = persis;
|
||||||
this.log = log;
|
this.log = log;
|
||||||
|
this.store = new Store(read, persis);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = this.request.headers.authorization.replace(
|
||||||
|
"Basic ",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const decodedValue = Buffer.from(value, "base64").toString("utf-8");
|
||||||
|
const [username, password] = decodedValue.split(":");
|
||||||
|
this.rc = new RcSdk(username, password, http, log);
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public id(): string {
|
public id(): string {
|
||||||
|
@ -57,20 +69,4 @@ export class Context {
|
||||||
}
|
}
|
||||||
return this.request.content;
|
return this.request.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkAuth() {
|
|
||||||
const authMode = await this.read
|
|
||||||
.getEnvironmentReader()
|
|
||||||
.getSettings()
|
|
||||||
.getValueById("auth-mode");
|
|
||||||
if (authMode === "bearer") {
|
|
||||||
const token = await this.read
|
|
||||||
.getEnvironmentReader()
|
|
||||||
.getSettings()
|
|
||||||
.getValueById("auth-bearer");
|
|
||||||
if (this.request.headers.authorization !== `Bearer ${token}`) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class GroupEndpoint extends ScimEndpoint implements IScimEndpoint {
|
||||||
const targetIds = new Set<string>(
|
const targetIds = new Set<string>(
|
||||||
SCIMGroup.fromPlain(ctx.content()).members.map((x) => x.value),
|
SCIMGroup.fromPlain(ctx.content()).members.map((x) => x.value),
|
||||||
);
|
);
|
||||||
targetIds.add(await ctx.rc.getUserId());
|
targetIds.add(ctx.rc.userId);
|
||||||
const currentIds = new Set<string>(
|
const currentIds = new Set<string>(
|
||||||
membersRaw.members.map((x) => x.user._id),
|
membersRaw.members.map((x) => x.user._id),
|
||||||
);
|
);
|
||||||
|
|
|
@ -117,7 +117,6 @@ export abstract class ScimEndpoint extends ApiEndpoint {
|
||||||
persis,
|
persis,
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
await ctx.checkAuth();
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`SCIM Request ${name.toUpperCase()} /${this.path}`,
|
`SCIM Request ${name.toUpperCase()} /${this.path}`,
|
||||||
request.content,
|
request.content,
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class UserEndpoint extends ScimEndpoint implements IScimEndpoint {
|
||||||
|
|
||||||
public async _put(ctx: Context): Promise<IApiResponse> {
|
public async _put(ctx: Context): Promise<IApiResponse> {
|
||||||
const u = SCIMUser.fromPlain(ctx.content());
|
const u = SCIMUser.fromPlain(ctx.content());
|
||||||
if (ctx.id() === (await ctx.rc.getUserId())) {
|
if (ctx.id() === ctx.rc.userId) {
|
||||||
u.active = true;
|
u.active = true;
|
||||||
if (!u.roles.find((x) => x.value === "admin")) {
|
if (!u.roles.find((x) => x.value === "admin")) {
|
||||||
u.roles.push({ value: "admin" });
|
u.roles.push({ value: "admin" });
|
||||||
|
@ -51,7 +51,7 @@ export class UserEndpoint extends ScimEndpoint implements IScimEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async _delete(ctx: Context): Promise<IApiResponse> {
|
public async _delete(ctx: Context): Promise<IApiResponse> {
|
||||||
if (ctx.id() === (await ctx.rc.getUserId())) {
|
if (ctx.id() === ctx.rc.userId) {
|
||||||
throw new SCIMError()
|
throw new SCIMError()
|
||||||
.setStatus(HttpStatusCode.FORBIDDEN)
|
.setStatus(HttpStatusCode.FORBIDDEN)
|
||||||
.setScimType(SCIMErrorType.MUTABILITY)
|
.setScimType(SCIMErrorType.MUTABILITY)
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { ConflictError } from "../../errors/ConflictError";
|
import { ConflictError } from "../../errors/ConflictError";
|
||||||
|
import { UnauthorizedError } from "../../errors/UnauthorizedError";
|
||||||
|
|
||||||
export function handleRcError(o: any) {
|
export function handleRcError(o: any) {
|
||||||
if (!o.success) {
|
if (!o.success) {
|
||||||
|
if (o.status === "error") {
|
||||||
|
if (o.message === "You must be logged in to do this.") {
|
||||||
|
throw new UnauthorizedError(o.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
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",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { SCIMError, SCIMErrorType } from "../scim/Error";
|
||||||
import { BaseError } from "./BaseError";
|
import { BaseError } from "./BaseError";
|
||||||
|
|
||||||
export class UnauthorizedError extends BaseError {
|
export class UnauthorizedError extends BaseError {
|
||||||
public message = "The bearer token is missing or doesn't match.";
|
public message = "The username or password is missing or incorrect.";
|
||||||
public toSCIMError(): SCIMError {
|
public toSCIMError(): SCIMError {
|
||||||
return new SCIMError()
|
return new SCIMError()
|
||||||
.setStatus(HttpStatusCode.UNAUTHORIZED)
|
.setStatus(HttpStatusCode.UNAUTHORIZED)
|
||||||
|
|
|
@ -20,14 +20,25 @@ export class RcSdk {
|
||||||
public user: RcSdkUser;
|
public user: RcSdkUser;
|
||||||
public team: RcSdkTeam;
|
public team: RcSdkTeam;
|
||||||
public role: RcSdkRole;
|
public role: RcSdkRole;
|
||||||
|
public readonly userId: string;
|
||||||
private readonly baseUrl = "http://localhost:3000/api/v1";
|
private readonly baseUrl = "http://localhost:3000/api/v1";
|
||||||
private readonly http: IHttp;
|
private readonly http: IHttp;
|
||||||
private readonly read: IRead;
|
|
||||||
private readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
|
private readonly headers: any;
|
||||||
|
|
||||||
constructor(http: IHttp, read: IRead, logger: ILogger) {
|
constructor(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
http: IHttp,
|
||||||
|
logger: ILogger,
|
||||||
|
) {
|
||||||
|
this.userId = username;
|
||||||
|
this.headers = {
|
||||||
|
"X-User-Id": username,
|
||||||
|
"X-Auth-Token": password,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.read = read;
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.user = new RcSdkUser(this);
|
this.user = new RcSdkUser(this);
|
||||||
this.team = new RcSdkTeam(this);
|
this.team = new RcSdkTeam(this);
|
||||||
|
@ -67,27 +78,13 @@ export class RcSdk {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserId(): Promise<string> {
|
|
||||||
return await this.read
|
|
||||||
.getEnvironmentReader()
|
|
||||||
.getSettings()
|
|
||||||
.getValueById("rc-user-id");
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildUrl(url: string): string {
|
private buildUrl(url: string): string {
|
||||||
return `${this.baseUrl}/${url}`;
|
return `${this.baseUrl}/${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildOptions(content?: any): Promise<IHttpRequest> {
|
private async buildOptions(content?: any): Promise<IHttpRequest> {
|
||||||
const options: IHttpRequest = {
|
const options: IHttpRequest = {
|
||||||
headers: {
|
headers: this.headers,
|
||||||
"X-User-Id": await this.getUserId(),
|
|
||||||
"X-Auth-Token": await this.read
|
|
||||||
.getEnvironmentReader()
|
|
||||||
.getSettings()
|
|
||||||
.getValueById("rc-token"),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
if (content !== undefined) {
|
if (content !== undefined) {
|
||||||
options.content = JSON.stringify(content);
|
options.content = JSON.stringify(content);
|
||||||
|
|
Reference in a new issue