mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: session sudo mode
This commit is contained in:
1 parent
52933de354
commit
f075301286
11 files changed
+125
-24
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/plugin-api",
|
"main": "src/plugin-api",
|
||||||
"module": "src/plugin-api",
|
"module": "src/plugin-api",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
|
|||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import {
|
import {
|
||||||
Handler, param, post, query, Types,
|
Handler, param, post, query, requireSudo, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { log2 } from '../utils';
|
import { log2 } from '../utils';
|
||||||
import { registerResolver, registerValue } from './api';
|
import { registerResolver, registerValue } from './api';
|
||||||
@@ -100,8 +100,8 @@ class DomainEditHandler extends ManageHandler {
|
|||||||
this.response.redirect = this.url('domain_dashboard');
|
this.response.redirect = this.url('domain_dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
async postDelete({ domainId, password }) {
|
@requireSudo
|
||||||
this.user.checkPassword(password);
|
async postDelete({ domainId }) {
|
||||||
if (domainId === 'system') throw new ForbiddenError('You are not allowed to delete system domain');
|
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.');
|
if (this.domain.owner !== this.user._id) throw new ForbiddenError('You are not the owner of this domain.');
|
||||||
await domain.del(domainId);
|
await domain.del(domainId);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import * as training from '../model/training';
|
|||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import {
|
import {
|
||||||
ConnectionHandler, Handler, param, query, Types,
|
ConnectionHandler, Handler, param, query, requireSudo, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { md5 } from '../utils';
|
import { md5 } from '../utils';
|
||||||
|
|
||||||
@@ -149,6 +149,7 @@ export class HomeHandler extends Handler {
|
|||||||
|
|
||||||
let geoip: Context['geoip'] = null;
|
let geoip: Context['geoip'] = null;
|
||||||
class HomeSecurityHandler extends Handler {
|
class HomeSecurityHandler extends Handler {
|
||||||
|
@requireSudo
|
||||||
async get() {
|
async get() {
|
||||||
// TODO(iceboy): pagination? or limit session count for uid?
|
// TODO(iceboy): pagination? or limit session count for uid?
|
||||||
const sessions = await token.getSessionListByUid(this.user._id);
|
const sessions = await token.getSessionListByUid(this.user._id);
|
||||||
@@ -169,6 +170,7 @@ class HomeSecurityHandler extends Handler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@requireSudo
|
||||||
@param('current', Types.String)
|
@param('current', Types.String)
|
||||||
@param('password', Types.String, isPassword)
|
@param('password', Types.String, isPassword)
|
||||||
@param('verifyPassword', Types.String)
|
@param('verifyPassword', Types.String)
|
||||||
@@ -180,6 +182,7 @@ class HomeSecurityHandler extends Handler {
|
|||||||
this.response.redirect = this.url('user_login');
|
this.response.redirect = this.url('user_login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@requireSudo
|
||||||
@param('password', Types.String)
|
@param('password', Types.String)
|
||||||
@param('mail', Types.Name, isEmail)
|
@param('mail', Types.Name, isEmail)
|
||||||
async postChangeMail(domainId: string, current: string, email: string) {
|
async postChangeMail(domainId: string, current: string, email: string) {
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import * as system from '../model/system';
|
|||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import {
|
import {
|
||||||
ConnectionHandler, Handler,
|
ConnectionHandler, Handler, param, requireSudo, Types,
|
||||||
param, Types,
|
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
import { configSource, saveConfig, SystemSettings } from '../settings';
|
import { configSource, saveConfig, SystemSettings } from '../settings';
|
||||||
import * as judge from './judge';
|
import * as judge from './judge';
|
||||||
@@ -142,6 +141,7 @@ class SystemScriptHandler extends SystemHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SystemSettingHandler extends SystemHandler {
|
class SystemSettingHandler extends SystemHandler {
|
||||||
|
@requireSudo
|
||||||
async get() {
|
async get() {
|
||||||
this.response.template = 'manage_setting.html';
|
this.response.template = 'manage_setting.html';
|
||||||
this.response.body.current = {};
|
this.response.body.current = {};
|
||||||
@@ -151,6 +151,7 @@ class SystemSettingHandler extends SystemHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@requireSudo
|
||||||
async post(args: any) {
|
async post(args: any) {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
const booleanKeys = args.booleanKeys || {};
|
const booleanKeys = args.booleanKeys || {};
|
||||||
@@ -179,6 +180,7 @@ class SystemSettingHandler extends SystemHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigHandler extends SystemHandler {
|
class SystemConfigHandler extends SystemHandler {
|
||||||
|
@requireSudo
|
||||||
async get() {
|
async get() {
|
||||||
this.response.template = 'manage_config.html';
|
this.response.template = 'manage_config.html';
|
||||||
let value = configSource;
|
let value = configSource;
|
||||||
@@ -191,6 +193,7 @@ class SystemConfigHandler extends SystemHandler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@requireSudo
|
||||||
@param('value', Types.String)
|
@param('value', Types.String)
|
||||||
async post(domainId: string, value: string) {
|
async post(domainId: string, value: string) {
|
||||||
let config;
|
let config;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import moment from 'moment-timezone';
|
|||||||
import notp from 'notp';
|
import notp from 'notp';
|
||||||
import b32 from 'thirty-two';
|
import b32 from 'thirty-two';
|
||||||
import {
|
import {
|
||||||
BlacklistedError, InvalidTokenError, LoginError,
|
BlacklistedError, ForbiddenError, InvalidTokenError, LoginError,
|
||||||
SystemError, UserAlreadyExistError, UserFacingError,
|
SystemError, UserAlreadyExistError, UserFacingError,
|
||||||
UserNotFoundError, ValidationError, VerifyPasswordError,
|
UserNotFoundError, ValidationError, VerifyPasswordError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
@@ -130,6 +130,7 @@ class UserLoginHandler extends Handler {
|
|||||||
if (!udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) throw new BlacklistedError(uname);
|
if (!udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) throw new BlacklistedError(uname);
|
||||||
this.session.viewLang = '';
|
this.session.viewLang = '';
|
||||||
this.session.uid = udoc._id;
|
this.session.uid = udoc._id;
|
||||||
|
this.session.sudo = null;
|
||||||
this.session.scope = PERM.PERM_ALL.toString();
|
this.session.scope = PERM.PERM_ALL.toString();
|
||||||
this.session.save = rememberme;
|
this.session.save = rememberme;
|
||||||
this.response.redirect = (redirect ? decodeURIComponent(redirect) : '')
|
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 {
|
class UserLogoutHandler extends Handler {
|
||||||
noCheckPermView = true;
|
noCheckPermView = true;
|
||||||
|
|
||||||
@@ -148,6 +177,7 @@ class UserLogoutHandler extends Handler {
|
|||||||
|
|
||||||
async post() {
|
async post() {
|
||||||
this.session.uid = 0;
|
this.session.uid = 0;
|
||||||
|
this.session.sudo = null;
|
||||||
this.session.scope = PERM.PERM_ALL.toString();
|
this.session.scope = PERM.PERM_ALL.toString();
|
||||||
this.response.redirect = '/';
|
this.response.redirect = '/';
|
||||||
}
|
}
|
||||||
@@ -425,6 +455,7 @@ class OauthCallbackHandler extends Handler {
|
|||||||
export async function apply(ctx) {
|
export async function apply(ctx) {
|
||||||
ctx.Route('user_login', '/login', UserLoginHandler);
|
ctx.Route('user_login', '/login', UserLoginHandler);
|
||||||
ctx.Route('user_oauth', '/oauth/:type', OauthHandler);
|
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_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
|
||||||
ctx.Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
|
ctx.Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
|
||||||
ctx.Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
|
ctx.Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ SystemSetting(
|
|||||||
Setting('setting_session', 'session.unsaved_expire_seconds', 3600 * 3,
|
Setting('setting_session', 'session.unsaved_expire_seconds', 3600 * 3,
|
||||||
'number', 'session.unsaved_expire_seconds', 'Unsaved session expire seconds'),
|
'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', '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),
|
Setting('setting_storage', 'installid', String.random(64), 'text', 'installid', 'Installation ID', FLAG_HIDDEN | FLAG_DISABLED),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { isSafeInteger } from 'lodash';
|
|||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { ObjectID } from 'mongodb';
|
import { ObjectID } from 'mongodb';
|
||||||
import saslprep from 'saslprep';
|
import saslprep from 'saslprep';
|
||||||
|
import { Time } from '@hydrooj/utils';
|
||||||
import { ValidationError } from '../error';
|
import { ValidationError } from '../error';
|
||||||
import { isContent, isTitle } from '../lib/validator';
|
import { isContent, isTitle } from '../lib/validator';
|
||||||
import type { Handler } from './server';
|
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 post: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'post', ...args));
|
||||||
export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args));
|
export const route: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'route', ...args));
|
||||||
export const param: DescriptorBuilder = (name, ...args) => _descriptor(_buildParam(name, 'all', ...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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.41.3",
|
"version": "4.41.4",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
|||||||
@@ -6,21 +6,15 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h1 class="section__title" data-heading>{{ _('Delete Domain') }}</h1>
|
<h1 class="section__title" data-heading>{{ _('Delete Domain') }}</h1>
|
||||||
|
<div class="section__tools">
|
||||||
|
<form method="POST">
|
||||||
|
<div>
|
||||||
|
<input type="hidden" name="operation" value="delete">
|
||||||
|
<input type="submit" value="{{ _('Delete Domain') }}" class="rounded primary button">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body"></div>
|
||||||
<form method="POST">
|
|
||||||
{{ form.form_text({
|
|
||||||
type:'password',
|
|
||||||
label:'Password',
|
|
||||||
columns:10,
|
|
||||||
name:'password',
|
|
||||||
required:true
|
|
||||||
}) }}
|
|
||||||
<div>
|
|
||||||
<input type="hidden" name="operation" value="delete">
|
|
||||||
<input type="submit" value="{{ _('Delete Domain') }}" class="rounded primary button">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends "layout/immersive.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row"><div class="columns">
|
||||||
|
<div class="immersive--content immersive--center">
|
||||||
|
<h1>{{ _('Confirm Access') }}</h1>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="row"><div class="columns">
|
||||||
|
<label class="inverse material textbox">
|
||||||
|
{{ _('Password') }}
|
||||||
|
<input name="password" type="password" autofocus>
|
||||||
|
</label>
|
||||||
|
</div></div>
|
||||||
|
<div class="row" style="display: none" id="tfa_div"><div class="columns">
|
||||||
|
<label class="inverse material textbox">
|
||||||
|
{{ _('Two Factor Authentication Code') }}
|
||||||
|
<input name="tfa" type="number">
|
||||||
|
</label>
|
||||||
|
</div></div>
|
||||||
|
<div class="row"><div class="columns">
|
||||||
|
<div class="text-center">
|
||||||
|
<input type="submit" value="{{ _('Confirm') }}" class="inverse expanded rounded primary button">
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
<div class="row"><div class="columns">
|
||||||
|
<div class="text-center supplementary inverse">
|
||||||
|
<p><strong>Tip:</strong> You are entering <a href="https://docs.github.com/articles/sudo-mode">sudo mode</a>.</p>
|
||||||
|
<p>After you've performed a sudo-protected action, you'll only be asked to re-authenticate again after a few hours of inactivity.</p>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends "layout/immersive.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row"><div class="columns">
|
||||||
|
<div class="immersive--content immersive--center">
|
||||||
|
<h1>{{ _('Confirm Access') }}</h1>
|
||||||
|
<div class="text-center">
|
||||||
|
Redirecting, please wait...
|
||||||
|
</div>
|
||||||
|
<form method="{{ method }}" action="{{ redirect }}" style="visibility:hidden">
|
||||||
|
{% for k, v in args %}
|
||||||
|
<input name="{{ k }}" value="{{ v }}" />
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" id="submit" />
|
||||||
|
</form>
|
||||||
|
<script>document.getElementById('submit').click()</script>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
{% endblock %}
|
||||||
Reference in new issue
Block a user