From f075301286ad5744a27506f15588579a2abdbcd4 Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 6 Oct 2022 16:11:02 +0800 Subject: [PATCH] core: session sudo mode --- packages/hydrooj/package.json | 2 +- packages/hydrooj/src/handler/domain.ts | 6 ++-- packages/hydrooj/src/handler/home.ts | 5 ++- packages/hydrooj/src/handler/manage.ts | 7 ++-- packages/hydrooj/src/handler/user.ts | 33 ++++++++++++++++++- packages/hydrooj/src/model/setting.ts | 1 + packages/hydrooj/src/service/decorators.ts | 18 ++++++++++ packages/ui-default/package.json | 2 +- .../ui-default/templates/domain_edit.html | 24 +++++--------- packages/ui-default/templates/user_sudo.html | 33 +++++++++++++++++++ .../templates/user_sudo_redirect.html | 18 ++++++++++ 11 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 packages/ui-default/templates/user_sudo.html create mode 100644 packages/ui-default/templates/user_sudo_redirect.html diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2276d21ec..ef1560f4f 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "4.0.4", + "version": "4.0.5", "bin": "bin/hydrooj.js", "main": "src/plugin-api", "module": "src/plugin-api", diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 228eb5bfd..d66497235 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -17,7 +17,7 @@ import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting'; import * as system from '../model/system'; import user from '../model/user'; import { - Handler, param, post, query, Types, + Handler, param, post, query, requireSudo, Types, } from '../service/server'; import { log2 } from '../utils'; import { registerResolver, registerValue } from './api'; @@ -100,8 +100,8 @@ class DomainEditHandler extends ManageHandler { this.response.redirect = this.url('domain_dashboard'); } - async postDelete({ domainId, password }) { - this.user.checkPassword(password); + @requireSudo + async postDelete({ domainId }) { if (domainId === 'system') throw new ForbiddenError('You are not allowed to delete system domain'); if (this.domain.owner !== this.user._id) throw new ForbiddenError('You are not the owner of this domain.'); await domain.del(domainId); diff --git a/packages/hydrooj/src/handler/home.ts b/packages/hydrooj/src/handler/home.ts index d89923c9e..52cad05d5 100644 --- a/packages/hydrooj/src/handler/home.ts +++ b/packages/hydrooj/src/handler/home.ts @@ -26,7 +26,7 @@ import * as training from '../model/training'; import user from '../model/user'; import * as bus from '../service/bus'; import { - ConnectionHandler, Handler, param, query, Types, + ConnectionHandler, Handler, param, query, requireSudo, Types, } from '../service/server'; import { md5 } from '../utils'; @@ -149,6 +149,7 @@ export class HomeHandler extends Handler { let geoip: Context['geoip'] = null; class HomeSecurityHandler extends Handler { + @requireSudo async get() { // TODO(iceboy): pagination? or limit session count for uid? const sessions = await token.getSessionListByUid(this.user._id); @@ -169,6 +170,7 @@ class HomeSecurityHandler extends Handler { }; } + @requireSudo @param('current', Types.String) @param('password', Types.String, isPassword) @param('verifyPassword', Types.String) @@ -180,6 +182,7 @@ class HomeSecurityHandler extends Handler { this.response.redirect = this.url('user_login'); } + @requireSudo @param('password', Types.String) @param('mail', Types.Name, isEmail) async postChangeMail(domainId: string, current: string, email: string) { diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index b11cafa93..2273fe570 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -14,8 +14,7 @@ import * as system from '../model/system'; import user from '../model/user'; import * as bus from '../service/bus'; import { - ConnectionHandler, Handler, - param, Types, + ConnectionHandler, Handler, param, requireSudo, Types, } from '../service/server'; import { configSource, saveConfig, SystemSettings } from '../settings'; import * as judge from './judge'; @@ -142,6 +141,7 @@ class SystemScriptHandler extends SystemHandler { } class SystemSettingHandler extends SystemHandler { + @requireSudo async get() { this.response.template = 'manage_setting.html'; this.response.body.current = {}; @@ -151,6 +151,7 @@ class SystemSettingHandler extends SystemHandler { } } + @requireSudo async post(args: any) { const tasks = []; const booleanKeys = args.booleanKeys || {}; @@ -179,6 +180,7 @@ class SystemSettingHandler extends SystemHandler { } class SystemConfigHandler extends SystemHandler { + @requireSudo async get() { this.response.template = 'manage_config.html'; let value = configSource; @@ -191,6 +193,7 @@ class SystemConfigHandler extends SystemHandler { }; } + @requireSudo @param('value', Types.String) async post(domainId: string, value: string) { let config; diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 30ca717a5..f5e7569f5 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -2,7 +2,7 @@ import moment from 'moment-timezone'; import notp from 'notp'; import b32 from 'thirty-two'; import { - BlacklistedError, InvalidTokenError, LoginError, + BlacklistedError, ForbiddenError, InvalidTokenError, LoginError, SystemError, UserAlreadyExistError, UserFacingError, UserNotFoundError, ValidationError, VerifyPasswordError, } from '../error'; @@ -130,6 +130,7 @@ class UserLoginHandler extends Handler { if (!udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) throw new BlacklistedError(uname); this.session.viewLang = ''; this.session.uid = udoc._id; + this.session.sudo = null; this.session.scope = PERM.PERM_ALL.toString(); this.session.save = rememberme; this.response.redirect = (redirect ? decodeURIComponent(redirect) : '') @@ -139,6 +140,34 @@ class UserLoginHandler extends Handler { } } +class UserSudoHandler extends Handler { + async get() { + if (!this.session.sudoArgs) throw new ForbiddenError(); + this.response.template = 'user_sudo.html'; + } + + @param('password', Types.String) + @param('tfa', Types.String, true) + async post(domainId: string, password: string, tfa = '') { + if (!this.session.sudoArgs) throw new ForbiddenError(); + await Promise.all([ + this.limitRate('user_sudo', 60, 5, true), + oplog.log(this, 'user.sudo', {}), + ]); + if (tfa) { + if (!this.user._tfa || !verifyToken(this.user._tfa, tfa)) throw new InvalidTokenError('2FA token invalid.'); + } else { + this.user.checkPassword(password); + } + this.session.sudo = Date.now(); + if (this.session.sudoArgs.method.toLowerCase() !== 'get') { + this.response.template = 'user_sudo_redirect.html'; + this.response.body = this.session.sudoArgs; + this.session.sudoArgs = null; + } else this.response.redirect = this.session.sudoArgs.redirect; + } +} + class UserLogoutHandler extends Handler { noCheckPermView = true; @@ -148,6 +177,7 @@ class UserLogoutHandler extends Handler { async post() { this.session.uid = 0; + this.session.sudo = null; this.session.scope = PERM.PERM_ALL.toString(); this.response.redirect = '/'; } @@ -425,6 +455,7 @@ class OauthCallbackHandler extends Handler { export async function apply(ctx) { ctx.Route('user_login', '/login', UserLoginHandler); ctx.Route('user_oauth', '/oauth/:type', OauthHandler); + ctx.Route('user_sudo', '/user/sudo', UserSudoHandler); ctx.Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler); ctx.Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER); ctx.Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER); diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 97f18892e..c936314fd 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -234,6 +234,7 @@ SystemSetting( Setting('setting_session', 'session.unsaved_expire_seconds', 3600 * 3, 'number', 'session.unsaved_expire_seconds', 'Unsaved session expire seconds'), Setting('setting_storage', 'db.ver', 0, 'number', 'db.ver', 'Database version', FLAG_DISABLED | FLAG_HIDDEN), + Setting('setting_storage', 'system.webmanage', 'disabled', 'text', 'system.webmanage', 'Enable web manage', FLAG_DISABLED | FLAG_HIDDEN), Setting('setting_storage', 'installid', String.random(64), 'text', 'installid', 'Installation ID', FLAG_HIDDEN | FLAG_DISABLED), ); diff --git a/packages/hydrooj/src/service/decorators.ts b/packages/hydrooj/src/service/decorators.ts index afae77956..ca85b52dd 100644 --- a/packages/hydrooj/src/service/decorators.ts +++ b/packages/hydrooj/src/service/decorators.ts @@ -4,6 +4,7 @@ import { isSafeInteger } from 'lodash'; import moment from 'moment-timezone'; import { ObjectID } from 'mongodb'; import saslprep from 'saslprep'; +import { Time } from '@hydrooj/utils'; import { ValidationError } from '../error'; import { isContent, isTitle } from '../lib/validator'; import type { Handler } from './server'; @@ -210,3 +211,20 @@ export const query: DescriptorBuilder = (name, ...args) => _descriptor(_buildPar export const post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args)); export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args)); export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...args)); + +export function requireSudo(target: any, funcName: string, obj: any) { + const originalMethod = obj.value; + obj.value = function sudo(this: Handler, ...args: any[]) { + if (this.session.sudo && Date.now() - this.session.sudo < Time.hour) { + return originalMethod.call(this, ...args); + } + this.session.sudoArgs = { + method: this.request.method, + args: this.args, + redirect: this.request.path, + }; + this.response.redirect = this.url('user_sudo'); + return 'cleanup'; + }; + return obj; +} diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 4377ef9d4..9601c43d5 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.41.3", + "version": "4.41.4", "author": "undefined ", "license": "AGPL-3.0", "main": "index.ts", diff --git a/packages/ui-default/templates/domain_edit.html b/packages/ui-default/templates/domain_edit.html index f3fd847f7..da349ec5e 100644 --- a/packages/ui-default/templates/domain_edit.html +++ b/packages/ui-default/templates/domain_edit.html @@ -6,21 +6,15 @@

{{ _('Delete Domain') }}

+
+
+
+ + +
+
+
-
-
- {{ form.form_text({ - type:'password', - label:'Password', - columns:10, - name:'password', - required:true - }) }} -
- - -
-
-
+
{% endblock %} diff --git a/packages/ui-default/templates/user_sudo.html b/packages/ui-default/templates/user_sudo.html new file mode 100644 index 000000000..42e6a98af --- /dev/null +++ b/packages/ui-default/templates/user_sudo.html @@ -0,0 +1,33 @@ +{% extends "layout/immersive.html" %} +{% block content %} +
+
+

{{ _('Confirm Access') }}

+
+
+ +
+ +
+
+ +
+
+
+
+

Tip: You are entering sudo mode.

+

After you've performed a sudo-protected action, you'll only be asked to re-authenticate again after a few hours of inactivity.

+
+
+
+
+
+{% endblock %} diff --git a/packages/ui-default/templates/user_sudo_redirect.html b/packages/ui-default/templates/user_sudo_redirect.html new file mode 100644 index 000000000..71601b9ac --- /dev/null +++ b/packages/ui-default/templates/user_sudo_redirect.html @@ -0,0 +1,18 @@ +{% extends "layout/immersive.html" %} +{% block content %} +
+
+

{{ _('Confirm Access') }}

+
+ Redirecting, please wait... +
+
+ {% for k, v in args %} + + {% endfor %} + +
+ +
+
+{% endblock %}