More work on API permissions
This commit is contained in:
parent
79b0adf81a
commit
c9a21c64bf
14 changed files with 318 additions and 85 deletions
|
@ -7,7 +7,7 @@ import pick from "lodash.pick";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import { ApiUserInfo } from "../data/ApiUserInfo";
|
import { ApiUserInfo } from "../data/ApiUserInfo";
|
||||||
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
import { ApiUserInfoData } from "../data/entities/ApiUserInfo";
|
||||||
import { ApiPermissions } from "../data/ApiPermissions";
|
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||||
import { ok } from "./responses";
|
import { ok } from "./responses";
|
||||||
|
|
||||||
interface IPassportApiUser {
|
interface IPassportApiUser {
|
||||||
|
@ -71,7 +71,7 @@ export function initAuth(app: express.Express) {
|
||||||
|
|
||||||
const apiLogins = new ApiLogins();
|
const apiLogins = new ApiLogins();
|
||||||
const apiUserInfo = new ApiUserInfo();
|
const apiUserInfo = new ApiUserInfo();
|
||||||
const apiPermissions = new ApiPermissions();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
|
|
||||||
// Initialize API tokens
|
// Initialize API tokens
|
||||||
passport.use(
|
passport.use(
|
||||||
|
@ -105,7 +105,7 @@ export function initAuth(app: express.Express) {
|
||||||
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
|
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
|
||||||
|
|
||||||
// Make sure the user is able to access at least 1 guild
|
// Make sure the user is able to access at least 1 guild
|
||||||
const permissions = await apiPermissions.getByUserId(user.id);
|
const permissions = await apiPermissionAssignments.getByUserId(user.id);
|
||||||
if (permissions.length === 0) {
|
if (permissions.length === 0) {
|
||||||
cb(null, {});
|
cb(null, {});
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,35 +1,38 @@
|
||||||
import express from "express";
|
import express, { Request, Response } from "express";
|
||||||
import passport from "passport";
|
|
||||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||||
import { ApiPermissions } from "../data/ApiPermissions";
|
import { clientError, ok, serverError, unauthorized } from "./responses";
|
||||||
import { clientError, error, ok, serverError, unauthorized } from "./responses";
|
|
||||||
import { Configs } from "../data/Configs";
|
import { Configs } from "../data/Configs";
|
||||||
import { ApiRoles } from "../data/ApiRoles";
|
|
||||||
import { validateGuildConfig } from "../configValidator";
|
import { validateGuildConfig } from "../configValidator";
|
||||||
import yaml, { YAMLException } from "js-yaml";
|
import yaml, { YAMLException } from "js-yaml";
|
||||||
import { apiTokenAuthHandlers } from "./auth";
|
import { apiTokenAuthHandlers } from "./auth";
|
||||||
|
import { ApiPermissions, hasPermission, permissionArrToSet } from "@shared/apiPermissions";
|
||||||
|
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||||
|
|
||||||
export function initGuildsAPI(app: express.Express) {
|
export function initGuildsAPI(app: express.Express) {
|
||||||
const allowedGuilds = new AllowedGuilds();
|
const allowedGuilds = new AllowedGuilds();
|
||||||
const apiPermissions = new ApiPermissions();
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
const configs = new Configs();
|
const configs = new Configs();
|
||||||
|
|
||||||
app.get("/guilds/available", ...apiTokenAuthHandlers(), async (req, res) => {
|
app.get("/guilds/available", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
||||||
const guilds = await allowedGuilds.getForApiUser(req.user.userId);
|
const guilds = await allowedGuilds.getForApiUser(req.user.userId);
|
||||||
res.json(guilds);
|
res.json(guilds);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req, res) => {
|
app.get("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
||||||
const permissions = await apiPermissions.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
||||||
if (!permissions) return unauthorized(res);
|
if (!permAssignment || !hasPermission(permissionArrToSet(permAssignment.permissions), ApiPermissions.ReadConfig)) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
const config = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
const config = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
||||||
res.json({ config: config ? config.config : "" });
|
res.json({ config: config ? config.config : "" });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req, res) => {
|
app.post("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req, res) => {
|
||||||
const permissions = await apiPermissions.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
||||||
if (!permissions || ApiRoles[permissions.role] < ApiRoles.Editor) return unauthorized(res);
|
if (!permAssignment || !hasPermission(permissionArrToSet(permAssignment.permissions), ApiPermissions.EditConfig)) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
let config = req.body.config;
|
let config = req.body.config;
|
||||||
if (config == null) return clientError(res, "No config supplied");
|
if (config == null) return clientError(res, "No config supplied");
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
import { BaseRepository } from "./BaseRepository";
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
import { ApiPermissionTypes } from "./ApiPermissionAssignments";
|
||||||
|
|
||||||
export class AllowedGuilds extends BaseRepository {
|
export class AllowedGuilds extends BaseRepository {
|
||||||
private allowedGuilds: Repository<AllowedGuild>;
|
private allowedGuilds: Repository<AllowedGuild>;
|
||||||
|
@ -33,8 +34,8 @@ export class AllowedGuilds extends BaseRepository {
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
"api_permissions",
|
"api_permissions",
|
||||||
"api_permissions",
|
"api_permissions",
|
||||||
"api_permissions.guild_id = allowed_guilds.id AND api_permissions.user_id = :userId",
|
"api_permissions.guild_id = allowed_guilds.id AND api_permissions.type = :type AND api_permissions.target_id = :userId",
|
||||||
{ userId },
|
{ type: ApiPermissionTypes.User, userId },
|
||||||
)
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
36
backend/src/data/ApiPermissionAssignments.ts
Normal file
36
backend/src/data/ApiPermissionAssignments.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { getRepository, Repository } from "typeorm";
|
||||||
|
import { ApiPermissionAssignment } from "./entities/ApiPermissionAssignment";
|
||||||
|
import { BaseRepository } from "./BaseRepository";
|
||||||
|
|
||||||
|
export enum ApiPermissionTypes {
|
||||||
|
User = "USER",
|
||||||
|
Role = "ROLE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiPermissionAssignments extends BaseRepository {
|
||||||
|
private apiPermissions: Repository<ApiPermissionAssignment>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.apiPermissions = getRepository(ApiPermissionAssignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByUserId(userId) {
|
||||||
|
return this.apiPermissions.find({
|
||||||
|
where: {
|
||||||
|
type: ApiPermissionTypes.User,
|
||||||
|
target_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getByGuildAndUserId(guildId, userId) {
|
||||||
|
return this.apiPermissions.findOne({
|
||||||
|
where: {
|
||||||
|
guild_id: guildId,
|
||||||
|
type: ApiPermissionTypes.User,
|
||||||
|
target_id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
import { getRepository, Repository } from "typeorm";
|
|
||||||
import { ApiPermission } from "./entities/ApiPermission";
|
|
||||||
import { BaseRepository } from "./BaseRepository";
|
|
||||||
|
|
||||||
export class ApiPermissions extends BaseRepository {
|
|
||||||
private apiPermissions: Repository<ApiPermission>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.apiPermissions = getRepository(ApiPermission);
|
|
||||||
}
|
|
||||||
|
|
||||||
getByUserId(userId) {
|
|
||||||
return this.apiPermissions.find({
|
|
||||||
where: {
|
|
||||||
user_id: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getByGuildAndUserId(guildId, userId) {
|
|
||||||
return this.apiPermissions.findOne({
|
|
||||||
where: {
|
|
||||||
guild_id: guildId,
|
|
||||||
user_id: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export enum ApiRoles {
|
|
||||||
Viewer = 1,
|
|
||||||
Editor,
|
|
||||||
Manager,
|
|
||||||
ServerOwner,
|
|
||||||
}
|
|
|
@ -2,19 +2,23 @@ import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from "typeorm";
|
||||||
import { ApiUserInfo } from "./ApiUserInfo";
|
import { ApiUserInfo } from "./ApiUserInfo";
|
||||||
|
|
||||||
@Entity("api_permissions")
|
@Entity("api_permissions")
|
||||||
export class ApiPermission {
|
export class ApiPermissionAssignment {
|
||||||
@Column()
|
@Column()
|
||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
user_id: string;
|
type: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
role: string;
|
@PrimaryColumn()
|
||||||
|
target_id: string;
|
||||||
|
|
||||||
@ManyToOne(type => ApiUserInfo, userInfo => userInfo.permissions)
|
@Column("simple-array")
|
||||||
@JoinColumn({ name: "user_id" })
|
permissions: string[];
|
||||||
|
|
||||||
|
@ManyToOne(type => ApiUserInfo, userInfo => userInfo.permissionAssignments)
|
||||||
|
@JoinColumn({ name: "target_id" })
|
||||||
userInfo: ApiUserInfo;
|
userInfo: ApiUserInfo;
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Entity, Column, PrimaryColumn, OneToMany } from "typeorm";
|
import { Entity, Column, PrimaryColumn, OneToMany } from "typeorm";
|
||||||
import { ApiLogin } from "./ApiLogin";
|
import { ApiLogin } from "./ApiLogin";
|
||||||
import { ApiPermission } from "./ApiPermission";
|
import { ApiPermissionAssignment } from "./ApiPermissionAssignment";
|
||||||
|
|
||||||
export interface ApiUserInfoData {
|
export interface ApiUserInfoData {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -23,6 +23,6 @@ export class ApiUserInfo {
|
||||||
@OneToMany(type => ApiLogin, login => login.userInfo)
|
@OneToMany(type => ApiLogin, login => login.userInfo)
|
||||||
logins: ApiLogin[];
|
logins: ApiLogin[];
|
||||||
|
|
||||||
@OneToMany(type => ApiPermission, perm => perm.userInfo)
|
@OneToMany(type => ApiPermissionAssignment, p => p.userInfo)
|
||||||
permissions: ApiPermission[];
|
permissionAssignments: ApiPermissionAssignment[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeorm";
|
||||||
|
|
||||||
|
export class AddTypeAndPermissionsToApiPermissions1573158035867 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.dropPrimaryKey("api_permissions");
|
||||||
|
await queryRunner.dropIndex("api_permissions", "IDX_5e371749d4cb4a5191f35e26f6");
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
"api_permissions",
|
||||||
|
new TableColumn({
|
||||||
|
name: "type",
|
||||||
|
type: "varchar",
|
||||||
|
length: "16",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.renameColumn("api_permissions", "user_id", "target_id");
|
||||||
|
|
||||||
|
await queryRunner.createPrimaryKey("api_permissions", ["guild_id", "type", "target_id"]);
|
||||||
|
|
||||||
|
await queryRunner.dropColumn("api_permissions", "role");
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
"api_permissions",
|
||||||
|
new TableColumn({
|
||||||
|
name: "permissions",
|
||||||
|
type: "text",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE api_permissions
|
||||||
|
SET type="USER",
|
||||||
|
permissions="EDIT_CONFIG"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"api_permissions",
|
||||||
|
new TableIndex({
|
||||||
|
columnNames: ["type", "target_id"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.dropIndex("api_permissions", "IDX_e06d750f13e6a4b4d3d6b847a9");
|
||||||
|
|
||||||
|
await queryRunner.dropColumn("api_permissions", "permissions");
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
"api_permissions",
|
||||||
|
new TableColumn({
|
||||||
|
name: "role",
|
||||||
|
type: "varchar",
|
||||||
|
length: "32",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.dropPrimaryKey("api_permissions");
|
||||||
|
|
||||||
|
await queryRunner.renameColumn("api_permissions", "target_id", "user_id");
|
||||||
|
|
||||||
|
await queryRunner.dropColumn("api_permissions", "type");
|
||||||
|
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"api_permissions",
|
||||||
|
new TableIndex({
|
||||||
|
columnNames: ["user_id"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createPrimaryKey("api_permissions", ["guild_id", "user_id"]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,5 +4,36 @@
|
||||||
<p>
|
<p>
|
||||||
<img class="inline-block w-16 mr-4" style="vertical-align: -20px" src="../../img/squint.png"> Or here
|
<img class="inline-block w-16 mr-4" style="vertical-align: -20px" src="../../img/squint.png"> Or here
|
||||||
</p>
|
</p>
|
||||||
|
<permission-tree :tree="tree" :granted-permissions="grantedPermissions" :on-change="onChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ApiPermissions, permissionHierarchy } from "@shared/apiPermissions";
|
||||||
|
import PermissionTree from "./PermissionTree.vue";
|
||||||
|
import { applyStateToPermissionHierarchy } from "./permissionTreeUtils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {PermissionTree},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tree: [],
|
||||||
|
grantedPermissions: new Set([ApiPermissions.EditConfig]),
|
||||||
|
managerPermissions: new Set([ApiPermissions.ManageAccess])
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateTreeState() {
|
||||||
|
this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
|
||||||
|
},
|
||||||
|
|
||||||
|
onChange() {
|
||||||
|
console.log('changed!', this.grantedPermissions);
|
||||||
|
this.updateTreeState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
62
dashboard/src/components/dashboard/PermissionTree.vue
Normal file
62
dashboard/src/components/dashboard/PermissionTree.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<ul class="nostyles">
|
||||||
|
<li v-for="[permission, treeState, subTree] in tree" :class="{locked: treeState.locked}">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
:checked="grantedPermissions.has(permission) || treeState.redundant"
|
||||||
|
v-on:input="togglePermission(permission)"
|
||||||
|
:disabled="treeState.locked || treeState.redundant">
|
||||||
|
<span>{{ permissionNames[permission] }}</span>
|
||||||
|
</label>
|
||||||
|
<permission-tree v-if="subTree && subTree.length"
|
||||||
|
:tree="subTree"
|
||||||
|
:granted-permissions="grantedPermissions"
|
||||||
|
:on-change="onChange" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
& ul {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked > label {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ApiPermissions, permissionNames } from "@shared/apiPermissions";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { TPermissionHierarchyWithState } from "./permissionTreeUtils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'permission-tree',
|
||||||
|
props: {
|
||||||
|
tree: Array as PropType<TPermissionHierarchyWithState>,
|
||||||
|
grantedPermissions: Set as PropType<Set<ApiPermissions>>,
|
||||||
|
onChange: Function
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { permissionNames };
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
togglePermission(permission) {
|
||||||
|
if (this.grantedPermissions.has(permission)) {
|
||||||
|
this.grantedPermissions.delete(permission);
|
||||||
|
} else {
|
||||||
|
this.grantedPermissions.add(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
43
dashboard/src/components/dashboard/permissionTreeUtils.ts
Normal file
43
dashboard/src/components/dashboard/permissionTreeUtils.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { ApiPermissions, hasPermission, TPermissionHierarchy } from "@shared/apiPermissions";
|
||||||
|
|
||||||
|
export type TPermissionHierarchyState = {
|
||||||
|
locked: boolean;
|
||||||
|
redundant: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TApiPermissionWithState = [ApiPermissions, TPermissionHierarchyState, TPermissionHierarchyWithState?];
|
||||||
|
export type TPermissionHierarchyWithState = TApiPermissionWithState[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tree
|
||||||
|
* @param grantedPermissions Permissions granted to the user being edited
|
||||||
|
* @param managerPermissions Permissions granted to the user who's editing the other user's permissions
|
||||||
|
* @param entireTreeIsGranted
|
||||||
|
*/
|
||||||
|
export function applyStateToPermissionHierarchy(
|
||||||
|
tree: TPermissionHierarchy,
|
||||||
|
grantedPermissions: Set<ApiPermissions>,
|
||||||
|
managerPermissions: Set<ApiPermissions> = new Set(),
|
||||||
|
entireTreeIsGranted = false,
|
||||||
|
): TPermissionHierarchyWithState {
|
||||||
|
const result: TPermissionHierarchyWithState = [];
|
||||||
|
|
||||||
|
for (const item of tree) {
|
||||||
|
const [perm, nested] = Array.isArray(item) ? item : [item];
|
||||||
|
|
||||||
|
// Can't edit permissions you don't have yourself
|
||||||
|
const locked = !hasPermission(managerPermissions, perm);
|
||||||
|
const permissionWithState: TApiPermissionWithState = [perm, { locked, redundant: entireTreeIsGranted }];
|
||||||
|
|
||||||
|
if (nested) {
|
||||||
|
const subTreeGranted = entireTreeIsGranted || grantedPermissions.has(perm);
|
||||||
|
permissionWithState.push(
|
||||||
|
applyStateToPermissionHierarchy(nested, grantedPermissions, managerPermissions, subTreeGranted),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(permissionWithState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -2,12 +2,12 @@ import { ApiPermissions, hasPermission } from "./apiPermissions";
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
|
|
||||||
test("Directly granted permissions match", t => {
|
test("Directly granted permissions match", t => {
|
||||||
t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.ManageAccess), true);
|
t.is(hasPermission(new Set([ApiPermissions.ManageAccess]), ApiPermissions.ManageAccess), true);
|
||||||
t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.Owner), false);
|
t.is(hasPermission(new Set([ApiPermissions.ManageAccess]), ApiPermissions.Owner), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Implicitly granted permissions by hierarchy match", t => {
|
test("Implicitly granted permissions by hierarchy match", t => {
|
||||||
t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.EditConfig), true);
|
t.is(hasPermission(new Set([ApiPermissions.ManageAccess]), ApiPermissions.EditConfig), true);
|
||||||
t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.ReadConfig), true);
|
t.is(hasPermission(new Set([ApiPermissions.ManageAccess]), ApiPermissions.ReadConfig), true);
|
||||||
t.is(hasPermission([ApiPermissions.EditConfig], ApiPermissions.ManageAccess), false);
|
t.is(hasPermission(new Set([ApiPermissions.EditConfig]), ApiPermissions.ManageAccess), false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,26 +5,36 @@ export enum ApiPermissions {
|
||||||
ReadConfig = "READ_CONFIG",
|
ReadConfig = "READ_CONFIG",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPermissionHierarchy extends Partial<Record<ApiPermissions, IPermissionHierarchy>> {}
|
const reverseApiPermissions = Object.entries(ApiPermissions).reduce((map, [key, value]) => {
|
||||||
|
map[value] = key;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
export const permissionHierarchy: IPermissionHierarchy = {
|
export const permissionNames = {
|
||||||
[ApiPermissions.Owner]: {
|
[ApiPermissions.Owner]: "Server owner",
|
||||||
[ApiPermissions.ManageAccess]: {
|
[ApiPermissions.ManageAccess]: "Manage dashboard access",
|
||||||
[ApiPermissions.EditConfig]: {
|
[ApiPermissions.EditConfig]: "Edit config",
|
||||||
[ApiPermissions.ReadConfig]: {},
|
[ApiPermissions.ReadConfig]: "Read config",
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TPermissionHierarchy = Array<ApiPermissions | [ApiPermissions, TPermissionHierarchy]>;
|
||||||
|
|
||||||
|
export const permissionHierarchy: TPermissionHierarchy = [
|
||||||
|
[ApiPermissions.Owner, [[ApiPermissions.ManageAccess, [[ApiPermissions.EditConfig, [ApiPermissions.ReadConfig]]]]]],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function permissionArrToSet(permissions: string[]): Set<ApiPermissions> {
|
||||||
|
return new Set(permissions.filter(p => reverseApiPermissions[p])) as Set<ApiPermissions>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether granted permissions include the specified permission, taking into account permission hierarchy i.e.
|
* Checks whether granted permissions include the specified permission, taking into account permission hierarchy i.e.
|
||||||
* that in the case of nested permissions, having a top level permission implicitly grants you any permissions nested
|
* that in the case of nested permissions, having a top level permission implicitly grants you any permissions nested
|
||||||
* under it as well
|
* under it as well
|
||||||
*/
|
*/
|
||||||
export function hasPermission(grantedPermissions: ApiPermissions[], permissionToCheck: ApiPermissions): boolean {
|
export function hasPermission(grantedPermissions: Set<ApiPermissions>, permissionToCheck: ApiPermissions): boolean {
|
||||||
// Directly granted
|
// Directly granted
|
||||||
if (grantedPermissions.includes(permissionToCheck)) {
|
if (grantedPermissions.has(permissionToCheck)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,15 +47,17 @@ export function hasPermission(grantedPermissions: ApiPermissions[], permissionTo
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTreeForPermission(
|
function checkTreeForPermission(
|
||||||
tree: IPermissionHierarchy,
|
tree: TPermissionHierarchy,
|
||||||
grantedPermissions: ApiPermissions[],
|
grantedPermissions: Set<ApiPermissions>,
|
||||||
permission: ApiPermissions,
|
permission: ApiPermissions,
|
||||||
): boolean {
|
): boolean {
|
||||||
for (const [perm, nested] of Object.entries(tree)) {
|
for (const item of tree) {
|
||||||
|
const [perm, nested] = Array.isArray(item) ? item : [item];
|
||||||
|
|
||||||
// Top-level permission granted, implicitly grant all nested permissions as well
|
// Top-level permission granted, implicitly grant all nested permissions as well
|
||||||
if (grantedPermissions.includes(perm as ApiPermissions)) {
|
if (grantedPermissions.has(perm)) {
|
||||||
// Permission we were looking for was found nested under this permission -> granted
|
// Permission we were looking for was found nested under this permission -> granted
|
||||||
if (treeIncludesPermission(nested, permission)) {
|
if (nested && treeIncludesPermission(nested, permission)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +67,7 @@ function checkTreeForPermission(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top-level permission not granted, check further nested permissions
|
// Top-level permission not granted, check further nested permissions
|
||||||
if (checkTreeForPermission(nested, grantedPermissions, permission)) {
|
if (nested && checkTreeForPermission(nested, grantedPermissions, permission)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,13 +75,15 @@ function checkTreeForPermission(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function treeIncludesPermission(tree: IPermissionHierarchy, permission: ApiPermissions): boolean {
|
function treeIncludesPermission(tree: TPermissionHierarchy, permission: ApiPermissions): boolean {
|
||||||
for (const [perm, nested] of Object.entries(tree)) {
|
for (const item of tree) {
|
||||||
|
const [perm, nested] = Array.isArray(item) ? item : [item];
|
||||||
|
|
||||||
if (perm === permission) {
|
if (perm === permission) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nestedResult = treeIncludesPermission(nested, permission);
|
const nestedResult = nested && treeIncludesPermission(nested, permission);
|
||||||
if (nestedResult) {
|
if (nestedResult) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue