move to basic auth

This commit is contained in:
Hugo Renard 2022-05-03 14:17:30 +02:00
parent 4cc01ed771
commit 0b1a83841d
Signed by: hougo
GPG key ID: 3A285FD470209C59
8 changed files with 39 additions and 85 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -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",

View file

@ -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)

View file

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