Skip to content

Commit

Permalink
Fix permission filter not being applied
Browse files Browse the repository at this point in the history
  • Loading branch information
zaidka committed Oct 11, 2019
1 parent 2085754 commit 306015f
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 32 deletions.
27 changes: 24 additions & 3 deletions lib/common/authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { PermissionSet, Expression } from "../types";
import { evaluate } from "./expression";
import { evaluate, or } from "./expression";

export default class Authorizer {
private permissionSets: PermissionSet[];
Expand All @@ -27,14 +27,16 @@ export default class Authorizer {
(mutationType, mutation, any) => boolean
>;
private hasAccessCache: Map<string, boolean>;
private getFilterCache: Map<string, Expression>;

public constructor(permissionSets) {
this.permissionSets = permissionSets;
this.validatorCache = new WeakMap();
this.hasAccessCache = new Map();
this.getFilterCache = new Map();
}

public hasAccess(resourceType, access): boolean {
public hasAccess(resourceType: string, access: number): boolean {
const cacheKey = `${resourceType}-${access}`;
if (this.hasAccessCache.has(cacheKey))
return this.hasAccessCache.get(cacheKey);
Expand All @@ -55,10 +57,29 @@ export default class Authorizer {
return has;
}

public getFilter(resourceType: string, access: number): Expression {
const cacheKey = `${resourceType}-${access}`;
if (this.getFilterCache.has(cacheKey))
return this.getFilterCache.get(cacheKey);

let filter: Expression = null;
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
if (perm[resourceType]) {
if (perm[resourceType].access >= access)
filter = or(filter, perm[resourceType].filter);
}
}
}

this.getFilterCache.set(cacheKey, filter);
return filter;
}

public getValidator(
resourceType,
resource
): (mutationType: string, mutation: any, args: any) => boolean {
): (mutationType: string, mutation?: any, args?: any) => boolean {
if (this.validatorCache.has(resource))
return this.validatorCache.get(resource);

Expand Down
4 changes: 2 additions & 2 deletions lib/common/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export async function evaluateAsync(
});
}

export function and(exp1, exp2): Expression {
export function and(exp1: Expression, exp2: Expression): Expression {
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
if (!isArray(exp2)) return exp2 ? exp1 : exp2;

Expand All @@ -292,7 +292,7 @@ export function and(exp1, exp2): Expression {
return res;
}

export function or(exp1, exp2): Expression {
export function or(exp1: Expression, exp2: Expression): Expression {
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
if (!isArray(exp2)) return exp2 ? exp2 : exp1;

Expand Down
90 changes: 63 additions & 27 deletions lib/ui/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { getConfig } from "../local-cache";
import { QueryOptions, Expression } from "../types";
import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache";
import Authorizer from "../common/authorizer";

const router = new Router();
export default router;
Expand Down Expand Up @@ -67,15 +68,20 @@ const resources = {
};

router.get(`/devices/:id.csv`, async ctx => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Query device (CSV)",
context: ctx,
id: ctx.params.id
};

const filter = ["=", ["PARAM", RESOURCE_IDS.devices], ctx.params.id];
const filter = and(authorizer.getFilter("devices", 2), [
"=",
["PARAM", RESOURCE_IDS.devices],
ctx.params.id
]);

if (!ctx.state.authorizer.hasAccess("devices", 2)) {
if (!authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 404);
}
Expand Down Expand Up @@ -115,8 +121,10 @@ router.get(`/devices/:id.csv`, async ctx => {

for (const [resource, flags] of Object.entries(resources)) {
router.head(`/${resource}`, async (ctx, next) => {
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
const authorizer: Authorizer = ctx.state.authorizer;
let filter: Expression = authorizer.getFilter(resource, 1);
if (ctx.request.query.filter)
filter = and(filter, parse(ctx.request.query.filter));

const log = {
message: `Count ${resource}`,
Expand All @@ -125,7 +133,7 @@ for (const [resource, flags] of Object.entries(resources)) {
count: null
};

if (!ctx.state.authorizer.hasAccess(resource, 1)) {
if (!authorizer.hasAccess(resource, 1)) {
logUnauthorizedWarning(log);
return void next();
}
Expand All @@ -148,9 +156,11 @@ for (const [resource, flags] of Object.entries(resources)) {
});

router.get(`/${resource}`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: QueryOptions = {};
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter)
filter = and(filter, parse(ctx.request.query.filter));
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort)
Expand All @@ -171,7 +181,7 @@ for (const [resource, flags] of Object.entries(resources)) {
projection: options.projection
};

if (!ctx.state.authorizer.hasAccess(resource, 2)) {
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
Expand Down Expand Up @@ -199,9 +209,11 @@ for (const [resource, flags] of Object.entries(resources)) {

// CSV download
router.get(`/${resource}.csv`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: QueryOptions = { projection: {} };
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter)
filter = and(filter, parse(ctx.request.query.filter));
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort)
Expand All @@ -216,7 +228,7 @@ for (const [resource, flags] of Object.entries(resources)) {
sort: options.sort
};

if (!ctx.state.authorizer.hasAccess(resource, 2)) {
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
Expand Down Expand Up @@ -285,14 +297,19 @@ for (const [resource, flags] of Object.entries(resources)) {
});

router.head(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Count ${resource}`,
context: ctx,
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
};

const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
const filter = and(authorizer.getFilter(resource, 2), [
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
Expand All @@ -306,14 +323,19 @@ for (const [resource, flags] of Object.entries(resources)) {
});

router.get(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Query ${resource}`,
context: ctx,
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
};

const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
const filter = and(authorizer.getFilter(resource, 2), [
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
Expand All @@ -326,17 +348,20 @@ for (const [resource, flags] of Object.entries(resources)) {
ctx.body = res[0];
});

// TODO add PUT, PATCH routes
if (flags & RESOURCE_DELETE) {
router.delete(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: `Delete ${resource}`,
context: ctx,
id: ctx.params.id
};

const authorizer = ctx.state.authorizer;
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
const filter = and(authorizer.getFilter(resource, 3), [
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id
]);
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
Expand All @@ -360,6 +385,7 @@ for (const [resource, flags] of Object.entries(resources)) {

if (flags & RESOURCE_PUT) {
router.put(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const id = ctx.params.id;

const log = {
Expand All @@ -368,7 +394,6 @@ for (const [resource, flags] of Object.entries(resources)) {
id: id
};

const authorizer = ctx.state.authorizer;
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
Expand Down Expand Up @@ -399,6 +424,7 @@ for (const [resource, flags] of Object.entries(resources)) {
}

router.put("/files/:id", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const resource = "files";
const id = ctx.params.id;

Expand All @@ -409,7 +435,6 @@ router.put("/files/:id", async (ctx, next) => {
metadata: null
};

const authorizer = ctx.state.authorizer;
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
Expand All @@ -436,15 +461,19 @@ router.put("/files/:id", async (ctx, next) => {
});

router.post("/devices/:id/tasks", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Commit tasks",
context: ctx,
deviceId: ctx.params.id,
tasks: null
};

const authorizer = ctx.state.authorizer;
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
const filter = and(authorizer.getFilter("devices", 3), [
"=",
["PARAM", "DeviceID.ID"],
ctx.params.id
]);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
Expand Down Expand Up @@ -481,15 +510,19 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
});

router.post("/devices/:id/tags", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Update tags",
context: ctx,
deviceId: ctx.params.id,
tags: ctx.request.body
};

const authorizer = ctx.state.authorizer;
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
const filter = and(authorizer.getFilter("devices", 3), [
"=",
["PARAM", "DeviceID.ID"],
ctx.params.id
]);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
Expand Down Expand Up @@ -528,15 +561,14 @@ router.get("/ping/:host", async ctx => {
});

router.put("/users/:id/password", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer;
const username = ctx.params.id;
const log = {
message: "Change password",
context: ctx,
username: username
};

const authorizer = ctx.state.authorizer;

if (!ctx.state.user) {
// User not logged in
if (
Expand All @@ -556,7 +588,11 @@ router.put("/users/:id/password", async (ctx, next) => {
return void next();
}

const filter = ["=", ["PARAM", RESOURCE_IDS.users], username];
const filter = and(authorizer.getFilter("users", 3), [
"=",
["PARAM", RESOURCE_IDS.users],
username
]);
const res = await db.query("users", filter);
if (!res.length) return void next();

Expand Down

0 comments on commit 306015f

Please sign in to comment.