mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add tfa enforce flow (#7805)
* add tfa enforce flow * add 'tfa-secret' to recommended permissions * fix if theme if user has dark mode * oas: rename 'enable-2fa' to 'enable-tfa' * Add required user fields * Uniformize styling * Fix direct and invalid routing * Add required permission docs * Fix typescript warnings * Fix typescript warnings 2 * Allow auto theme * Nest duplicate condition check * Fix routing for users without role * Follow page redirects * Reduce the use of redirect query * Improve error UX * Allow admins to disable 2FA * Improve autofocus UX * Override update permission for 'tfa_secret' when role enforces TFA * Remove permission requirements from docs Co-authored-by: Jose Varela <joselcvarela@gmail.com> Co-authored-by: rijkvanzanten <rijkvanzanten@me.com> Co-authored-by: ian <licitdev@gmail.com>
This commit is contained in:
@@ -4,9 +4,10 @@ import { InvalidCredentialsException, ForbiddenException, InvalidPayloadExceptio
|
||||
import { respond } from '../middleware/respond';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { validateBatch } from '../middleware/validate-batch';
|
||||
import { AuthenticationService, MetaService, UsersService, TFAService } from '../services';
|
||||
import { AuthenticationService, MetaService, UsersService, RolesService, TFAService } from '../services';
|
||||
import { PrimaryKey } from '../types';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { Role } from '@directus/shared/types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -354,6 +355,37 @@ router.post(
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
|
||||
// Override permissions only when enforce TFA is enabled in role
|
||||
if (req.accountability.role) {
|
||||
const rolesService = new RolesService({
|
||||
schema: req.schema,
|
||||
});
|
||||
const role = (await rolesService.readOne(req.accountability.role)) as Role;
|
||||
|
||||
if (role && role.enforce_tfa) {
|
||||
const existingPermission = await req.accountability.permissions?.find(
|
||||
(p) => p.collection === 'directus_users' && p.action === 'update'
|
||||
);
|
||||
|
||||
if (existingPermission) {
|
||||
existingPermission.fields = ['tfa_secret'];
|
||||
existingPermission.permissions = { id: { _eq: req.accountability.user } };
|
||||
existingPermission.presets = null;
|
||||
existingPermission.validation = null;
|
||||
} else {
|
||||
(req.accountability.permissions || (req.accountability.permissions = [])).push({
|
||||
action: 'update',
|
||||
collection: 'directus_users',
|
||||
fields: ['tfa_secret'],
|
||||
permissions: { id: { _eq: req.accountability.user } },
|
||||
presets: null,
|
||||
role: req.accountability.role,
|
||||
validation: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const service = new TFAService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
@@ -377,6 +409,37 @@ router.post(
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
|
||||
// Override permissions only when enforce TFA is enabled in role
|
||||
if (req.accountability.role) {
|
||||
const rolesService = new RolesService({
|
||||
schema: req.schema,
|
||||
});
|
||||
const role = (await rolesService.readOne(req.accountability.role)) as Role;
|
||||
|
||||
if (role && role.enforce_tfa) {
|
||||
const existingPermission = await req.accountability.permissions?.find(
|
||||
(p) => p.collection === 'directus_users' && p.action === 'update'
|
||||
);
|
||||
|
||||
if (existingPermission) {
|
||||
existingPermission.fields = ['tfa_secret'];
|
||||
existingPermission.permissions = { id: { _eq: req.accountability.user } };
|
||||
existingPermission.presets = null;
|
||||
existingPermission.validation = null;
|
||||
} else {
|
||||
(req.accountability.permissions || (req.accountability.permissions = [])).push({
|
||||
action: 'update',
|
||||
collection: 'directus_users',
|
||||
fields: ['tfa_secret'],
|
||||
permissions: { id: { _eq: req.accountability.user } },
|
||||
presets: null,
|
||||
role: req.accountability.role,
|
||||
validation: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const service = new TFAService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
@@ -394,4 +457,26 @@ router.post(
|
||||
respond
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:pk/tfa/disable',
|
||||
asyncHandler(async (req, _res, next) => {
|
||||
if (!req.accountability?.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if (!req.accountability.admin || !req.params.pk) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const service = new TFAService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.disableTFA(req.params.pk);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user