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