使用 BigInt 进行权限管理

This commit is contained in:
undefined committed 2020-07-14 22:03:54 +08:00
1 parent eaed0e84f5
commit 8aef05a375
17 files changed
+252 -197

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ module.exports = {
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
BigInt: true,
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
-1
View File
@@ -1,5 +1,4 @@
@requireCsrfToken
为model.system添加缓存
bug修复
使用BigInt管理权限
修复discussion前端权限节点显示异常
+13 -2
View File
@@ -114,6 +114,13 @@ class DomainUserHandler extends ManageHandler {
class DomainPermissionHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId);
const log2 = (val: bigint) => {
// @ts-ignore
for (let i = 0n; ; i++) {
// @ts-ignore
if ((val >> i) === 0n) return parseInt(i.toString(), 10) - 1;
}
};
const path = [
['Hydro', 'homepage'],
['domain', null],
@@ -121,7 +128,7 @@ class DomainPermissionHandler extends ManageHandler {
];
this.response.template = 'domain_permission.html';
this.response.body = {
roles, PERMS_BY_FAMILY, domain: this.domain, path,
roles, PERMS_BY_FAMILY, domain: this.domain, path, log2,
};
}
@@ -129,7 +136,11 @@ class DomainPermissionHandler extends ManageHandler {
const roles = {};
for (const role in this.request.body) {
if (this.request.body[role] instanceof Array) {
roles[role] = this.request.body[role].join('');
const perms = this.request.body[role];
// @ts-ignore
roles[role] = 0n;
// @ts-ignore
for (const r of perms) roles[role] += 1n << BigInt(r);
}
}
await domain.setRoles(domainId, roles);
+1 -1
View File
@@ -82,7 +82,7 @@ class StatusHandler extends Handler {
class StatusUpdateHandler extends Handler {
async post(args) {
this.checkPerm(PRIV.PRIV_JUDGE);
this.checkPriv(PRIV.PRIV_JUDGE);
args.type = 'judger';
return coll.updateOne(
{ mid: args.mid, type: 'judger' },
+8 -14
View File
@@ -20,7 +20,7 @@ import {
NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError,
} from '../error';
import { Pdoc, Udoc, Rdoc } from '../interface';
import { Pdoc, User, Rdoc } from '../interface';
const parseCategory = (value: string) => {
if (!value) return [];
@@ -165,7 +165,7 @@ class ProblemRandomHandler extends ProblemHandler {
class ProblemDetailHandler extends ProblemHandler {
pdoc: Pdoc;
udoc: Udoc;
udoc: User;
@param('pid', Types.String, null, parsePid)
async _prepare(domainId: string, pid: number | string) {
@@ -457,12 +457,9 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
async postEditReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(psdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION))) {
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY);
}
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY);
}
await solution.editReply(domainId, psid, psrid, content);
this.back();
@@ -473,12 +470,9 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
async postDeleteReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(psdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION))) {
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY);
}
if (!(psrdoc.owner === this.user._id
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) {
this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY);
}
await solution.delReply(domainId, psid, psrid);
this.back();
+6 -2
View File
@@ -172,9 +172,13 @@ class UserDetailHandler extends Handler {
sdoc.updateIp = '';
sdoc._id = '';
}
const path = [
['Hydro', 'homepage'],
['user_detail', 'user_detail', { uid }],
];
this.response.template = 'user_detail.html';
this.response.body = {
isSelfProfile, udoc, sdoc, rdocs, pdict,
isSelfProfile, udoc, sdoc, rdocs, pdict, path,
};
}
}
@@ -321,7 +325,7 @@ class OauthCallbackHandler extends Handler {
r.uname.push(String.random(16));
for (const uname of r.uname) {
// eslint-disable-next-line no-await-in-loop
const nudoc = await user.getByUname('system', uname, true);
const nudoc = await user.getByUname('system', uname);
if (!nudoc) {
username = uname;
break;
+19 -3
View File
@@ -15,6 +15,22 @@ export interface Setting {
}
export interface Udoc extends Dictionary<any> {
_id: number,
mail: string,
mailLower: string,
uname: string,
unameLower: string,
salt: string,
hash: string,
hashType: string,
priv: number,
regat: Date,
loginat: Date,
regip: string,
loginip: string,
}
export interface User extends Dictionary<any> {
udoc: () => any,
dudoc: () => any,
_id: number,
@@ -26,16 +42,16 @@ export interface Udoc extends Dictionary<any> {
priv: number,
regat: Date,
loginat: Date,
perm: string,
perm: bigint,
role: string,
regip: () => string,
loginip: () => string,
hasPerm: (perm: string) => boolean,
hasPerm: (perm: bigint) => boolean,
hasPriv: (priv: number) => boolean,
checkPassword: (password: string) => void,
}
export type Udict = NumericDictionary<Udoc>;
export type Udict = NumericDictionary<User>;
export interface Pdoc {
_id: ObjectID,
+5 -5
View File
@@ -4,7 +4,7 @@ import { PERM, PRIV } from '../model/builtin';
global.Hydro.ui.nav = [];
const trueChecker = () => true;
const Checker = (perm: string, priv: number, checker: Function = trueChecker) => (handler) => (
const Checker = (perm: bigint, priv: number, checker: Function = trueChecker) => (handler) => (
checker(handler)
&& (perm ? handler.user.hasPerm(perm) : true)
&& (priv ? handler.user.hasPriv(priv) : true)
@@ -12,10 +12,10 @@ const Checker = (perm: string, priv: number, checker: Function = trueChecker) =>
const Item = (
name: string, args: any, prefix: string,
...permPrivChecker: Array<number | string | Function | Array<number | string>>
...permPrivChecker: Array<number | bigint | Function | Array<bigint | string>>
) => {
let _priv: number;
let _perm: string;
let _perm: bigint;
let checker: Function = trueChecker;
for (const item of permPrivChecker) {
if (item instanceof Object && !isNull(item)) {
@@ -23,7 +23,7 @@ const Item = (
if (typeof item[0] === 'number') {
// @ts-ignore
_priv = item;
} else if (typeof item[0] === 'string') {
} else if (typeof item[0] === 'bigint') {
// @ts-ignore
_perm = item;
}
@@ -32,7 +32,7 @@ const Item = (
}
} else if (typeof item === 'number') {
_priv = item;
} else if (typeof item === 'string') {
} else if (typeof item === 'bigint') {
_perm = item;
}
}
+2
View File
@@ -1,5 +1,6 @@
import fs from 'fs';
import path from 'path';
import serialize from 'serialize-javascript';
import nunjucks from 'nunjucks';
import * as markdown from './markdown';
import { STATUS, PERM, PRIV } from '../model/builtin';
@@ -33,6 +34,7 @@ class Nunjucks extends nunjucks.Environment {
constructor() {
super(new Loader(), { autoescape: true, trimBlocks: true });
this.addFilter('json', (self) => JSON.stringify(self), false);
this.addFilter('serialize', (self, ignoreFunction = true) => serialize(self, { ignoreFunction }));
this.addFilter('assign', (self, data) => Object.assign(self, data));
this.addFilter('markdown', (self, safe = true) => {
if (safe) return markdown.safe.render(self);
+122 -118
View File
@@ -1,91 +1,95 @@
// @ts-nocheck
/* Why nocheck?
* BitInt requires at least ES2020, but we can't use it as
* NodeJS doesn't support some of this new features.
* e.g.: object?.property
*/
export const PERM = {
PERM_NONE: '',
PERM_NONE: 0n,
// Domain Settings
PERM_VIEW: '0',
PERM_EDIT_DOMAIN: '1',
PERM_MOD_BADGE: '2',
PERM_VIEW: 1n << 0n,
PERM_EDIT_DOMAIN: 1n << 1n,
PERM_MOD_BADGE: 1n << 2n,
// Problem and Record
PERM_CREATE_PROBLEM: '3',
PERM_EDIT_PROBLEM: '4',
PERM_EDIT_PROBLEM_SELF: '5',
PERM_VIEW_PROBLEM: '6',
PERM_VIEW_PROBLEM_HIDDEN: '7',
PERM_SUBMIT_PROBLEM: '8',
PERM_READ_PROBLEM_DATA: '9',
PERM_READ_PROBLEM_DATA_SELF: 'A',
PERM_READ_RECORD_CODE: 'B',
PERM_REJUDGE_PROBLEM: 'C',
PERM_REJUDGE: 'D',
PERM_CREATE_PROBLEM: 1n << 4n,
PERM_EDIT_PROBLEM: 1n << 5n,
PERM_EDIT_PROBLEM_SELF: 1n << 6n,
PERM_VIEW_PROBLEM: 1n << 7n,
PERM_VIEW_PROBLEM_HIDDEN: 1n << 8n,
PERM_SUBMIT_PROBLEM: 1n << 9n,
PERM_READ_PROBLEM_DATA: 1n << 10n,
PERM_READ_PROBLEM_DATA_SELF: 1n << 11n,
PERM_READ_RECORD_CODE: 1n << 12n,
PERM_REJUDGE_PROBLEM: 1n << 13n,
PERM_REJUDGE: 1n << 14n,
// Problem Solution
PERM_VIEW_PROBLEM_SOLUTION: 'E',
PERM_CREATE_PROBLEM_SOLUTION: 'F',
PERM_VOTE_PROBLEM_SOLUTION: 'G',
PERM_EDIT_PROBLEM_SOLUTION: 'H',
PERM_EDIT_PROBLEM_SOLUTION_SELF: 'I',
PERM_DELETE_PROBLEM_SOLUTION: 'J',
PERM_DELETE_PROBLEM_SOLUTION_SELF: 'K',
PERM_REPLY_PROBLEM_SOLUTION: 'L',
PERM_EDIT_PROBLEM_SOLUTION_REPLY: 'M',
PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 'N',
PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION: 'x',
PERM_DELETE_PROBLEM_SOLUTION_REPLY: 'O',
PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 'P',
PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION: 'y',
PERM_VIEW_PROBLEM_SOLUTION: 1n << 15n,
PERM_CREATE_PROBLEM_SOLUTION: 1n << 16n,
PERM_VOTE_PROBLEM_SOLUTION: 1n << 17n,
PERM_EDIT_PROBLEM_SOLUTION: 1n << 18n,
PERM_EDIT_PROBLEM_SOLUTION_SELF: 1n << 19n,
PERM_DELETE_PROBLEM_SOLUTION: 1n << 20n,
PERM_DELETE_PROBLEM_SOLUTION_SELF: 1n << 21n,
PERM_REPLY_PROBLEM_SOLUTION: 1n << 22n,
PERM_EDIT_PROBLEM_SOLUTION_REPLY: 1n << 23n,
PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 1n << 24n,
PERM_DELETE_PROBLEM_SOLUTION_REPLY: 1n << 25n,
PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 1n << 26n,
// Discussion
PERM_VIEW_DISCUSSION: 'Q',
PERM_CREATE_DISCUSSION: 'R',
PERM_HIGHLIGHT_DISCUSSION: 'S',
PERM_EDIT_DISCUSSION: 'T',
PERM_EDIT_DISCUSSION_SELF: 'U',
PERM_DELETE_DISCUSSION: 'V',
PERM_DELETE_DISCUSSION_SELF: 'W',
PERM_REPLY_DISCUSSION: 'X',
PERM_EDIT_DISCUSSION_REPLY: 'Y',
PERM_EDIT_DISCUSSION_REPLY_SELF: 'Z',
PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 'a',
PERM_DELETE_DISCUSSION_REPLY: 'b',
PERM_DELETE_DISCUSSION_REPLY_SELF: 'c',
PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 'd',
PERM_VIEW_DISCUSSION: 1n << 27n,
PERM_CREATE_DISCUSSION: 1n << 28n,
PERM_HIGHLIGHT_DISCUSSION: 1n << 29n,
PERM_EDIT_DISCUSSION: 1n << 30n,
PERM_EDIT_DISCUSSION_SELF: 1n << 31n,
PERM_DELETE_DISCUSSION: 1n << 32n,
PERM_DELETE_DISCUSSION_SELF: 1n << 33n,
PERM_REPLY_DISCUSSION: 1n << 34n,
PERM_EDIT_DISCUSSION_REPLY: 1n << 35n,
PERM_EDIT_DISCUSSION_REPLY_SELF: 1n << 36n,
PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 37n,
PERM_DELETE_DISCUSSION_REPLY: 1n << 38n,
PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n,
PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n,
// Contest
PERM_VIEW_CONTEST: 'e',
PERM_VIEW_CONTEST_SCOREBOARD: 'f',
PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 'g',
PERM_CREATE_CONTEST: 'h',
PERM_ATTEND_CONTEST: 'i',
PERM_EDIT_CONTEST: 'j',
PERM_EDIT_CONTEST_SELF: 'k',
PERM_VIEW_CONTEST: 1n << 41n,
PERM_VIEW_CONTEST_SCOREBOARD: 1n << 42n,
PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 1n << 43n,
PERM_CREATE_CONTEST: 1n << 44n,
PERM_ATTEND_CONTEST: 1n << 45n,
PERM_EDIT_CONTEST: 1n << 50n,
PERM_EDIT_CONTEST_SELF: 1n << 51n,
// Homework
PERM_VIEW_HOMEWORK: 'l',
PERM_VIEW_HOMEWORK_SCOREBOARD: 'm',
PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 'n',
PERM_CREATE_HOMEWORK: 'o',
PERM_ATTEND_HOMEWORK: 'p',
PERM_EDIT_HOMEWORK: 'q',
PERM_EDIT_HOMEWORK_SELF: 'r',
PERM_VIEW_HOMEWORK: 1n << 52n,
PERM_VIEW_HOMEWORK_SCOREBOARD: 1n << 53n,
PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 1n << 54n,
PERM_CREATE_HOMEWORK: 1n << 55n,
PERM_ATTEND_HOMEWORK: 1n << 56n,
PERM_EDIT_HOMEWORK: 1n << 57n,
PERM_EDIT_HOMEWORK_SELF: 1n << 58n,
// Training
PERM_VIEW_TRAINING: 's',
PERM_CREATE_TRAINING: 't',
PERM_EDIT_TRAINING: 'u',
PERM_EDIT_TRAINING_SELF: 'v',
PERM_VIEW_TRAINING: 1n << 46n,
PERM_CREATE_TRAINING: 1n << 47n,
PERM_EDIT_TRAINING: 1n << 48n,
PERM_EDIT_TRAINING_SELF: 1n << 49n,
// Ranking
PERM_VIEW_RANKING: 'w',
PERM_VIEW_RANKING: 1n << 50n,
// Placeholder
PERM_ALL: '',
PERM_BASIC: '',
PERM_DEFAULT: '',
PERM_ADMIN: '',
PERM_ALL: 0n,
PERM_BASIC: 0n,
PERM_DEFAULT: 0n,
PERM_ADMIN: 0n,
};
export const Permission = (family: string, key: string, desc: string) => ({ family, key, desc });
export const Permission = (family: string, key: BigInt, desc: string) => ({ family, key, desc });
export const PERMS = [
Permission('perm_general', PERM.PERM_VIEW, 'View this domain'),
@@ -149,7 +153,7 @@ export const PERMS = [
Permission('perm_ranking', PERM.PERM_VIEW_RANKING, 'View ranking'),
];
let PERM_ALL = '';
let PERM_ALL = 0n;
for (const p in PERM) if (PERM[p]) PERM_ALL += PERM[p];
PERM.PERM_ALL = PERM_ALL;
@@ -160,59 +164,59 @@ for (const p of PERMS) {
}
PERM.PERM_BASIC = PERM.PERM_VIEW
+ PERM.PERM_VIEW_PROBLEM
+ PERM.PERM_VIEW_PROBLEM_SOLUTION
+ PERM.PERM_VIEW_DISCUSSION
+ PERM.PERM_VIEW_CONTEST
+ PERM.PERM_VIEW_CONTEST_SCOREBOARD
+ PERM.PERM_VIEW_HOMEWORK
+ PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
+ PERM.PERM_VIEW_TRAINING
+ PERM.PERM_VIEW_RANKING;
| PERM.PERM_VIEW_PROBLEM
| PERM.PERM_VIEW_PROBLEM_SOLUTION
| PERM.PERM_VIEW_DISCUSSION
| PERM.PERM_VIEW_CONTEST
| PERM.PERM_VIEW_CONTEST_SCOREBOARD
| PERM.PERM_VIEW_HOMEWORK
| PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
| PERM.PERM_VIEW_TRAINING
| PERM.PERM_VIEW_RANKING;
PERM.PERM_DEFAULT = PERM.PERM_VIEW
+ PERM.PERM_VIEW_PROBLEM
+ PERM.PERM_EDIT_PROBLEM_SELF
+ PERM.PERM_SUBMIT_PROBLEM
+ PERM.PERM_READ_PROBLEM_DATA_SELF
+ PERM.PERM_VIEW_PROBLEM_SOLUTION
+ PERM.PERM_CREATE_PROBLEM_SOLUTION
+ PERM.PERM_VOTE_PROBLEM_SOLUTION
+ PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF
+ PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF
+ PERM.PERM_REPLY_PROBLEM_SOLUTION
+ PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF
+ PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF
+ PERM.PERM_VIEW_DISCUSSION
+ PERM.PERM_CREATE_DISCUSSION
+ PERM.PERM_EDIT_DISCUSSION_SELF
+ PERM.PERM_REPLY_DISCUSSION
+ PERM.PERM_EDIT_DISCUSSION_REPLY_SELF
| PERM.PERM_VIEW_PROBLEM
| PERM.PERM_EDIT_PROBLEM_SELF
| PERM.PERM_SUBMIT_PROBLEM
| PERM.PERM_READ_PROBLEM_DATA_SELF
| PERM.PERM_VIEW_PROBLEM_SOLUTION
| PERM.PERM_CREATE_PROBLEM_SOLUTION
| PERM.PERM_VOTE_PROBLEM_SOLUTION
| PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF
| PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF
| PERM.PERM_REPLY_PROBLEM_SOLUTION
| PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF
| PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF
| PERM.PERM_VIEW_DISCUSSION
| PERM.PERM_CREATE_DISCUSSION
| PERM.PERM_EDIT_DISCUSSION_SELF
| PERM.PERM_REPLY_DISCUSSION
| PERM.PERM_EDIT_DISCUSSION_REPLY_SELF
// PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION
+ PERM.PERM_DELETE_DISCUSSION_REPLY_SELF
+ PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION
+ PERM.PERM_VIEW_CONTEST
+ PERM.PERM_VIEW_CONTEST_SCOREBOARD
+ PERM.PERM_ATTEND_CONTEST
+ PERM.PERM_EDIT_CONTEST_SELF
+ PERM.PERM_VIEW_HOMEWORK
+ PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
+ PERM.PERM_ATTEND_HOMEWORK
+ PERM.PERM_EDIT_HOMEWORK_SELF
+ PERM.PERM_VIEW_TRAINING
+ PERM.PERM_CREATE_TRAINING
+ PERM.PERM_EDIT_TRAINING_SELF
+ PERM.PERM_VIEW_RANKING
+ PERM.PERM_SUBMIT_PROBLEM
+ PERM.PERM_CREATE_PROBLEM_SOLUTION
+ PERM.PERM_VOTE_PROBLEM_SOLUTION
+ PERM.PERM_REPLY_PROBLEM_SOLUTION
+ PERM.PERM_CREATE_DISCUSSION
+ PERM.PERM_REPLY_DISCUSSION
+ PERM.PERM_ATTEND_CONTEST
+ PERM.PERM_CREATE_TRAINING
+ PERM.PERM_ATTEND_HOMEWORK
+ PERM.PERM_VIEW_RANKING;
| PERM.PERM_DELETE_DISCUSSION_REPLY_SELF
| PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION
| PERM.PERM_VIEW_CONTEST
| PERM.PERM_VIEW_CONTEST_SCOREBOARD
| PERM.PERM_ATTEND_CONTEST
| PERM.PERM_EDIT_CONTEST_SELF
| PERM.PERM_VIEW_HOMEWORK
| PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
| PERM.PERM_ATTEND_HOMEWORK
| PERM.PERM_EDIT_HOMEWORK_SELF
| PERM.PERM_VIEW_TRAINING
| PERM.PERM_CREATE_TRAINING
| PERM.PERM_EDIT_TRAINING_SELF
| PERM.PERM_VIEW_RANKING
| PERM.PERM_SUBMIT_PROBLEM
| PERM.PERM_CREATE_PROBLEM_SOLUTION
| PERM.PERM_VOTE_PROBLEM_SOLUTION
| PERM.PERM_REPLY_PROBLEM_SOLUTION
| PERM.PERM_CREATE_DISCUSSION
| PERM.PERM_REPLY_DISCUSSION
| PERM.PERM_ATTEND_CONTEST
| PERM.PERM_CREATE_TRAINING
| PERM.PERM_ATTEND_HOMEWORK
| PERM.PERM_VIEW_RANKING;
PERM.PERM_ADMIN = PERM.PERM_ALL;
+14 -12
View File
@@ -8,7 +8,7 @@ const collUser = db.collection('domain.user');
export async function add(domainId: string, owner: number, name: string, bulletin: string) {
await coll.insertOne({
_id: domainId, owner, name, bulletin, roles: BUILTIN_ROLES,
_id: domainId, owner, name, bulletin, roles: {},
});
return domainId;
}
@@ -50,14 +50,14 @@ export function setUserRole(domainId: string, uid: number, role: string) {
return collUser.updateOne({ uid, domainId }, { role }, { upsert: true });
}
export async function getRoles(arg: string | any) {
let ddoc;
if (typeof arg === 'string') ddoc = get(arg);
export async function getRoles(arg: string | DomainDoc) {
let ddoc: DomainDoc;
if (typeof arg === 'string') ddoc = await get(arg);
else ddoc = arg;
const roles = [];
const r = [];
for (const role in ddoc.roles) {
roles.push({ _id: role, perm: ddoc.roles[role] });
roles.push({ _id: role, perm: BigInt(ddoc.roles[role]) });
r.push(role);
}
for (const role in BUILTIN_ROLES) {
@@ -68,18 +68,18 @@ export async function getRoles(arg: string | any) {
return roles;
}
export async function setRoles(domainId: string, roles: any) {
export async function setRoles(domainId: string, roles: Dictionary<bigint>) {
const current = await get(domainId);
for (const role in roles) {
current.roles[role] = roles[role];
current.roles[role] = roles[role].toString();
}
return await coll.updateOne({ _id: domainId }, { $set: { roles: current } });
return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } });
}
export async function addRole(domainId: string, name: string, permission: string) {
export async function addRole(domainId: string, name: string, permission: bigint) {
const current = await get(domainId);
current.roles[name] = permission;
return await coll.updateOne({ _id: domainId }, { $set: { roles: current } });
current.roles[name] = permission.toString();
return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } });
}
export async function deleteRoles(domainId: string, roles: string[]) {
@@ -98,7 +98,9 @@ export async function getDomainUser(domainId: string, udoc: Udoc) {
if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin';
dudoc.role = dudoc.role || 'default';
const ddoc = await get(domainId);
dudoc.perm = ddoc.roles[dudoc.role] || BUILTIN_ROLES[dudoc.role];
dudoc.perm = ddoc.roles[dudoc.role]
? BigInt(ddoc.roles[dudoc.role])
: BUILTIN_ROLES[dudoc.role];
return dudoc;
}
+15 -16
View File
@@ -6,19 +6,21 @@ import { BUILTIN_USERS, PRIV } from './builtin';
import { UserNotFoundError, UserAlreadyExistError, LoginError } from '../error';
import pwhash from '../lib/hash.hydro';
import * as db from '../service/db';
import { Udoc, Udict } from '../interface';
import { User as _User, Udoc, Udict } from '../interface';
const coll = db.collection('user');
export function setPassword(uid: number, password: string) {
export async function setPassword(uid: number, password: string): Promise<Udoc> {
const salt = String.random();
return coll.findOneAndUpdate(
const res = await coll.findOneAndUpdate(
{ _id: uid },
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
{ returnOriginal: false },
);
return res.value;
}
export class User implements Udoc {
export class User implements _User {
udoc: () => any;
dudoc: () => any;
@@ -41,7 +43,7 @@ export class User implements Udoc {
loginat: Date;
perm: string;
perm: bigint;
role: string;
@@ -49,6 +51,8 @@ export class User implements Udoc {
loginip: () => string;
[key: string]: any;
constructor(udoc, dudoc) {
this.udoc = () => udoc;
this.dudoc = () => dudoc;
@@ -81,8 +85,8 @@ export class User implements Udoc {
}
}
hasPerm(p: string) {
return this.perm.includes(p);
hasPerm(p: bigint) {
return (this.perm & p) === p;
}
hasPriv(p: number) {
@@ -100,7 +104,7 @@ export class User implements Udoc {
}
}
export async function getById(domainId: string, _id: number): Promise<Udoc | null> {
export async function getById(domainId: string, _id: number): Promise<User | null> {
const udoc = _id === 0 || _id === 1
? BUILTIN_USERS[_id]
: await coll.findOne({ _id });
@@ -117,24 +121,19 @@ export async function getList(domainId: string, uids: number[]): Promise<Udict>
return r;
}
export async function getByUname(
domainId: string, uname: string, ignoreMissing = false,
): Promise<Udoc> {
export async function getByUname(domainId: string, uname: string): Promise<User | null> {
const unameLower = uname.trim().toLowerCase();
const udoc = (unameLower === 'guest')
? BUILTIN_USERS[0]
: unameLower === 'hydro'
? BUILTIN_USERS[1]
: await coll.findOne({ unameLower });
if (!udoc) {
if (ignoreMissing) return null;
throw new UserNotFoundError(uname);
}
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
return new User(udoc, dudoc);
}
export async function getByEmail(domainId: string, mail: string): Promise<Udoc | null> {
export async function getByEmail(domainId: string, mail: string): Promise<User | null> {
const mailLower = mail.trim().toLowerCase();
const udoc = (mailLower === 'guest@hydro.local')
? BUILTIN_USERS[0]
+35 -20
View File
@@ -12,12 +12,14 @@ import Body from 'koa-body';
import Router from 'koa-router';
import cache from 'koa-static-cache';
import sockjs from 'sockjs';
import serialize from 'serialize-javascript';
import { lrucache } from '../utils';
import { Udoc, DomainDoc } from '../interface';
import { User, DomainDoc } from '../interface';
import {
UserNotFoundError, BlacklistedError, PermissionError,
UserFacingError, ValidationError, PrivilegeError,
CsrfTokenError, InvalidOperationError, MethodNotAllowedError, NotFoundError,
CsrfTokenError, InvalidOperationError, MethodNotAllowedError,
NotFoundError,
} from '../error';
import { render } from '../lib/template';
import * as misc from '../lib/misc';
@@ -222,7 +224,7 @@ export class Handler {
csrfToken: string;
user: Udoc;
user: User;
domain: DomainDoc;
@@ -266,7 +268,11 @@ export class Handler {
async renderHTML(name: string, context: any): Promise<string> {
if (enableLog) console.time(name);
const UserContext = { ...this.user, gravatar: misc.gravatar(this.user.gravatar || '', 128) };
const UserContext = {
...this.user,
gravatar: misc.gravatar(this.user.gravatar || '', 128),
perm: this.user.perm.toString(),
};
const res = await render(name, {
handler: this,
UserContext,
@@ -291,7 +297,7 @@ export class Handler {
return `${this.translate(str)} - Hydro`;
}
checkPerm(...args) {
checkPerm(...args: Array<bigint[] | bigint>) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
@@ -302,13 +308,14 @@ export class Handler {
}
}
if (!p) throw new PermissionError([args[i]]);
// @ts-ignore
} else if (!this.user.hasPerm(args[i])) {
throw new PermissionError([[args[i]]]);
}
}
}
checkPriv(...args) {
checkPriv(...args: Array<number[] | number>) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
@@ -319,6 +326,7 @@ export class Handler {
}
}
if (!p) throw new PrivilegeError([args[i]]);
// @ts-ignore
} else if (!this.user.hasPriv(args[i])) {
throw new PrivilegeError([[args[i]]]);
}
@@ -412,14 +420,16 @@ export class Handler {
}
async renderBody() {
if (!this.response.redirect && !this.request.json) {
if (this.response.body || this.response.template) {
if (this.request.query.noTemplate || this.request.json) return;
const templateName = this.request.query.template || this.response.template;
if (templateName) {
this.response.body = this.response.body || {};
await this.render(templateName, this.response.body);
}
if (this.response.type) return;
if (this.request.json || this.response.redirect || !this.response.template) {
this.response.body = serialize(this.response.body);
this.response.type = 'application/json';
} else if (this.response.body || this.response.template) {
if (this.request.query.noTemplate || this.request.json) return;
const templateName = this.request.query.template || this.response.template;
if (templateName) {
this.response.body = this.response.body || {};
await this.render(templateName, this.response.body);
}
}
}
@@ -535,7 +545,12 @@ async function handle(ctx, HandlerClass, checker) {
if (h.cleanup) await h.cleanup(args);
if (h.finish) await h.finish(args);
} catch (e) {
await h.onerror(e);
try {
await h.onerror(e);
} catch (err) {
h.response.code = 500;
h.response.body = `${err.message}\n${err.stack}`;
}
}
}
@@ -549,12 +564,12 @@ const Checker = (permPrivChecker) => {
checker = item;
} if (typeof item[0] === 'number') {
priv = item;
} else if (typeof item[0] === 'string') {
} else if (typeof item[0] === 'bigint') {
perm = item;
}
} else if (typeof item === 'number') {
priv = item;
} else if (typeof item === 'string') {
} else if (typeof item === 'bigint') {
perm = item;
}
}
@@ -620,7 +635,7 @@ export class ConnectionHandler {
return `${this.translate(str)} - Hydro`;
}
checkPerm(...args) {
checkPerm(...args: Array<bigint[] | bigint>) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
@@ -637,7 +652,7 @@ export class ConnectionHandler {
}
}
checkPriv(...args) {
checkPriv(...args: Array<number[] | number>) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
@@ -703,7 +718,7 @@ export class ConnectionHandler {
export function Connection(
name: string, prefix: string,
RouteConnHandler: any,
...permPrivChecker: Array<number | string | Function>
...permPrivChecker: Array<number | bigint | Function>
) {
const sock = sockjs.createServer({ prefix });
const checker = Checker(permPrivChecker);
+2 -1
View File
@@ -32,6 +32,7 @@
"nunjucks": "^3.2.1",
"prismjs": "^1.20.0",
"saslprep": "^1.0.3",
"serialize-javascript": "^4.0.0",
"sockjs": "^0.3.20",
"superagent": "^5.2.2",
"superagent-proxy": "^2.0.0",
@@ -88,4 +89,4 @@
]
},
"snyk": true
}
}
+1 -1
View File
@@ -36,7 +36,7 @@
{% for role in roles %}
<td class="col--p">
<label class="compact checkbox">
<input name="{{ role._id }}" data-checkbox-group="{{ role._id }}" data-checkbox-range value="{{ perm.key }}" type="checkbox"{% if role.perm.includes(perm.key) %} checked{% endif %}{% if role._id == 'admin' %} disabled{% endif %}>
<input name="{{ role._id }}" data-checkbox-group="{{ role._id }}" data-checkbox-range value="{{ log2(perm.key) }}" type="checkbox"{% if role.perm|bitand(perm.key) %} checked{% endif %}{% if role._id == 'admin' %} disabled{% endif %}>
</label>
</td>
{% endfor %}
+1 -1
View File
@@ -8,7 +8,7 @@
/ {% if loop.index < path.length %}<a href="{{ url(c[1], c[2])|default('#') }}">{{ c[0] if c[3] else _(c[0]) }}</a>{% endif %}
{%- endfor %}
</div>
<h1 class="location-current" data-emoji-enabled>{{ path[path.length - 1][0] if path[path.length - 1][2] else _(path[path.length - 1][0]) }}</h1>
<h1 class="location-current" data-emoji-enabled>{{ path[path.length - 1][0] if path[path.length - 1][3] else _(path[path.length - 1][0]) }}</h1>
</div>
<div class="media__right">
{% include "partials/hamburger.html" %}
+7
View File
@@ -5642,6 +5642,13 @@ serialize-javascript@^3.1.0:
dependencies:
randombytes "^2.1.0"
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
dependencies:
randombytes "^2.1.0"
set-immediate-shim@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"