Skip to content

Commit

Permalink
feat: Allow users to set admins when initialising the user dashboard …
Browse files Browse the repository at this point in the history
…recipe (#691)

* Allow users to set emails as admins for user dashboard

* Change dashboard logic to always allow sign out regardless of admins array

* Update CHANGELOG

* Fix debug log

* Update version

* Update to consume email from core response

* Update debug message

* Update package version
  • Loading branch information
nkshah2 authored Sep 12, 2023
1 parent cfe4441 commit f2de049
Show file tree
Hide file tree
Showing 20 changed files with 273 additions and 22 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [15.2.0] - 2023-09-11

### Added

- The Dashboard recipe now accepts a new `admins` property which can be used to give Dashboard Users write privileges for the user dashboard.

### Changes

- Dashboard APIs now return a status code `403` for all non-GET requests if the currently logged in Dashboard User is not listed in the `admins` array

## [15.1.1] - 2023-08-14

### Fixes
Expand Down
42 changes: 37 additions & 5 deletions lib/build/recipe/dashboard/api/apiKeyProtector.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,47 @@ var __awaiter =
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
const error_1 = __importDefault(require("../error"));
const utils_1 = require("../utils");
function apiKeyProtector(apiImplementation, tenantId, options, apiFunction, userContext) {
return __awaiter(this, void 0, void 0, function* () {
const shouldAllowAccess = yield options.recipeImplementation.shouldAllowAccess({
req: options.req,
config: options.config,
userContext,
});
let shouldAllowAccess = false;
try {
shouldAllowAccess = yield options.recipeImplementation.shouldAllowAccess({
req: options.req,
config: options.config,
userContext,
});
} catch (e) {
if (error_1.default.isErrorFromSuperTokens(e) && e.type === error_1.default.OPERATION_NOT_ALLOWED) {
options.res.setStatusCode(403);
options.res.sendJSONResponse({
message: e.message,
});
return true;
}
throw e;
}
if (!shouldAllowAccess) {
utils_1.sendUnauthorisedAccess(options.res);
return true;
Expand Down
6 changes: 6 additions & 0 deletions lib/build/recipe/dashboard/error.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @ts-nocheck
import STError from "../../error";
export default class DashboardError extends STError {
static OPERATION_NOT_ALLOWED: "OPERATION_NOT_ALLOWED";
constructor(message?: string);
}
32 changes: 32 additions & 0 deletions lib/build/recipe/dashboard/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use strict";
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
const error_1 = __importDefault(require("../../error"));
class DashboardError extends error_1.default {
constructor(message) {
super({
message: message !== undefined ? message : "You are not permitted to perform this operation",
type: DashboardError.OPERATION_NOT_ALLOWED,
});
}
}
exports.default = DashboardError;
DashboardError.OPERATION_NOT_ALLOWED = "OPERATION_NOT_ALLOWED";
47 changes: 44 additions & 3 deletions lib/build/recipe/dashboard/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ var __importDefault =
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const error_1 = __importDefault(require("./error"));
const logger_1 = require("../../logger");
const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath"));
const querier_1 = require("../../querier");
const utils_1 = require("../../utils");
const version_1 = require("../../version");
const utils_1 = require("./utils");
const constants_1 = require("./constants");
const utils_2 = require("./utils");
function getRecipeImplementation() {
return {
getDashboardBundleLocation: function () {
Expand All @@ -78,9 +82,46 @@ function getRecipeImplementation() {
sessionId: authHeaderValue,
}
);
return sessionVerificationResponse.status === "OK";
if (sessionVerificationResponse.status !== "OK") {
return false;
}
// For all non GET requests we also want to check if the user is allowed to perform this operation
if (utils_1.normaliseHttpMethod(input.req.getMethod()) !== "get") {
// We dont want to block the analytics API
if (input.req.getOriginalURL().endsWith(constants_1.DASHBOARD_ANALYTICS_API)) {
return true;
}
// We do not want to block the sign out request
if (input.req.getOriginalURL().endsWith(constants_1.SIGN_OUT_API)) {
return true;
}
const admins = input.config.admins;
if (admins === undefined) {
return true;
}
if (admins.length === 0) {
logger_1.logDebugMessage(
"User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin"
);
throw new error_1.default();
}
const userEmail = sessionVerificationResponse.email;
if (userEmail === undefined || typeof userEmail !== "string") {
logger_1.logDebugMessage(
"User Dashboard: Returning Unauthorised because no email was returned from the core. Should never come here"
);
return false;
}
if (!admins.includes(userEmail)) {
logger_1.logDebugMessage(
"User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin"
);
throw new error_1.default();
}
}
return true;
}
return yield utils_1.validateApiKey(input);
return yield utils_2.validateApiKey(input);
});
},
};
Expand Down
2 changes: 2 additions & 0 deletions lib/build/recipe/dashboard/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseRequest, BaseResponse } from "../../framework";
import { NormalisedAppinfo } from "../../types";
export declare type TypeInput = {
apiKey?: string;
admins?: string[];
override?: {
functions?: (
originalImplementation: RecipeInterface,
Expand All @@ -14,6 +15,7 @@ export declare type TypeInput = {
};
export declare type TypeNormalisedInput = {
apiKey?: string;
admins?: string[];
authMode: AuthMode;
override: {
functions: (
Expand Down
12 changes: 12 additions & 0 deletions lib/build/recipe/dashboard/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const thirdpartyemailpassword_1 = __importDefault(require("../thirdpartyemailpas
const recipe_4 = __importDefault(require("../thirdpartyemailpassword/recipe"));
const thirdpartypasswordless_1 = __importDefault(require("../thirdpartypasswordless"));
const recipe_5 = __importDefault(require("../thirdpartypasswordless/recipe"));
const logger_1 = require("../../logger");
function validateAndNormaliseUserInput(config) {
let override = Object.assign(
{
Expand All @@ -71,9 +72,20 @@ function validateAndNormaliseUserInput(config) {
},
config === undefined ? {} : config.override
);
if (
(config === null || config === void 0 ? void 0 : config.apiKey) !== undefined &&
(config === null || config === void 0 ? void 0 : config.admins) !== undefined
) {
logger_1.logDebugMessage("User Dashboard: Providing 'admins' has no effect when using an apiKey.");
}
let admins;
if ((config === null || config === void 0 ? void 0 : config.admins) !== undefined) {
admins = config.admins.map((email) => utils_1.normaliseEmail(email));
}
return Object.assign(Object.assign({}, config), {
override,
authMode: config !== undefined && config.apiKey ? "api-key" : "email-password",
admins,
});
}
exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput;
Expand Down
1 change: 1 addition & 0 deletions lib/build/utils.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion lib/build/utils.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 20 additions & 5 deletions lib/ts/recipe/dashboard/api/apiKeyProtector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
import RecipeError from "../error";
import { APIFunction, APIInterface, APIOptions } from "../types";
import { sendUnauthorisedAccess } from "../utils";

Expand All @@ -22,11 +23,25 @@ export default async function apiKeyProtector(
apiFunction: APIFunction,
userContext: any
): Promise<boolean> {
const shouldAllowAccess = await options.recipeImplementation.shouldAllowAccess({
req: options.req,
config: options.config,
userContext,
});
let shouldAllowAccess = false;

try {
shouldAllowAccess = await options.recipeImplementation.shouldAllowAccess({
req: options.req,
config: options.config,
userContext,
});
} catch (e) {
if (RecipeError.isErrorFromSuperTokens(e) && e.type === RecipeError.OPERATION_NOT_ALLOWED) {
options.res.setStatusCode(403);
options.res.sendJSONResponse({
message: e.message,
});
return true;
}

throw e;
}

if (!shouldAllowAccess) {
sendUnauthorisedAccess(options.res);
Expand Down
26 changes: 26 additions & 0 deletions lib/ts/recipe/dashboard/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import STError from "../../error";

export default class DashboardError extends STError {
static OPERATION_NOT_ALLOWED: "OPERATION_NOT_ALLOWED" = "OPERATION_NOT_ALLOWED";

constructor(message?: string) {
super({
message: message !== undefined ? message : "You are not permitted to perform this operation",
type: DashboardError.OPERATION_NOT_ALLOWED,
});
}
}
49 changes: 48 additions & 1 deletion lib/ts/recipe/dashboard/recipeImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
* under the License.
*/

import RecipeError from "./error";
import { logDebugMessage } from "../../logger";
import NormalisedURLPath from "../../normalisedURLPath";
import { Querier } from "../../querier";
import { normaliseHttpMethod } from "../../utils";
import { dashboardVersion } from "../../version";
import { DASHBOARD_ANALYTICS_API, SIGN_OUT_API } from "./constants";
import { RecipeInterface } from "./types";
import { validateApiKey } from "./utils";

Expand All @@ -36,7 +40,50 @@ export default function getRecipeImplementation(): RecipeInterface {
sessionId: authHeaderValue,
}
);
return sessionVerificationResponse.status === "OK";

if (sessionVerificationResponse.status !== "OK") {
return false;
}

// For all non GET requests we also want to check if the user is allowed to perform this operation
if (normaliseHttpMethod(input.req.getMethod()) !== "get") {
// We dont want to block the analytics API
if (input.req.getOriginalURL().endsWith(DASHBOARD_ANALYTICS_API)) {
return true;
}

// We do not want to block the sign out request
if (input.req.getOriginalURL().endsWith(SIGN_OUT_API)) {
return true;
}

const admins = input.config.admins;

if (admins === undefined) {
return true;
}

if (admins.length === 0) {
logDebugMessage("User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin");
throw new RecipeError();
}

const userEmail = sessionVerificationResponse.email;

if (userEmail === undefined || typeof userEmail !== "string") {
logDebugMessage(
"User Dashboard: Returning Unauthorised because no email was returned from the core. Should never come here"
);
return false;
}

if (!admins.includes(userEmail)) {
logDebugMessage("User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin");
throw new RecipeError();
}
}

return true;
}
return await validateApiKey(input);
},
Expand Down
Loading

0 comments on commit f2de049

Please sign in to comment.