Skip to content
This repository was archived by the owner on Jan 6, 2023. It is now read-only.

Commit c698bf5

Browse files
Release v2.5.0 (#1222)
* Issue Fix #1180 (#1183) * Issue fix #1191 (#1192) * Issue fix #1196 (#1197) * Add 2fa authentication (#1031) * Parent + Nested validation changes (#1138) * Add migration schema for 2FA Secret user field * Add 2fa_secret field to FieldsSeeder * Create Missing 2FA Password Exception * Add googleauthenticator dependency * Add getter for User's 2FA secret * Check for otp param in login request, and login with it * Add enforce_2fa parameter to directus_settings * Create Utils endpoint and service method to generate 2fa secret * Add enforce_2fa field to roles * Add enforce_2fa field to FieldsSeeder * Change Missing2FAPasswordException error code to 111 * Change 2FA Library * Change 2fa_secret interface in FieldsSeeder * Created exception for invalid otp * Changed findUserWithCredentials to through an InvalidOTPException on otp check * Created new exception if 2fa is enforced but not enabled by user * Added function to check if 2fa is enforced for a user * Check in AuthenticationMiddleware whether 2fa is enforced and enabled for user * Add optional needs2FA field to auth token and on token refresh * Catch error if enforce_2fa column doesn't exist Fixes crash when has2FAEnforced is called on a DB that hasn't been migrated * Use relative positions for target path array to check user edit * Fix unset on payload_arr instead of payload * Change 2FA activation on login to use activate2FA endpoint * Update ItemsService.php * Issue Fix #1194 (#1195) * Issue Fix #1194 * Update comment * Valildation issue of O2M/M2O at insertion (#1198) * Fox #1201 (#1202) * Fix #1203 (#1204) * Update collections() method in types.php (#1184) There are cases when $type is not a string but an object that inherits from ObjectType. In that situation array_key_exists failing because it should get only integers or strings as a first parameter. So in order to avoid that the 'name' property of the object is used as a key. * Improve YouTube Embed Provider (#1210) Adds in detection and parsing for youtu.be shorthand URLs. * Add check for environment on bootstrap (#1215) * Fix #1186 [Create new error code for invalid login entity] (#1218) * Fix #1217 (Changing password over the CLI doesn't work) (#1220) * Feature/audio video upload (#1214) * added file meta data for audio/video * updates as per PR feedback * Fix #1207 [Permission denied issue when using translation interface] (#1221) * Bump version to v2.5.0
1 parent 77a95be commit c698bf5

34 files changed

+558
-92
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
"ext-mbstring": "*",
4040
"ext-openssl": "*",
4141
"ext-gd": "*",
42-
"webonyx/graphql-php": "^0.13.0"
42+
"webonyx/graphql-php": "^0.13.0",
43+
"char0n/ffmpeg-php": "^3.0.0",
44+
"pragmarx/google2fa": "^5.0"
4345
},
4446
"require-dev": {
4547
"phpunit/phpunit": "^5.7.25",

migrations/db/seeds/FieldsSeeder.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,14 @@ public function run()
15671567
'readonly' => 1,
15681568
'hidden_detail' => 1
15691569
],
1570+
[
1571+
'collection' => 'directus_users',
1572+
'field' => '2fa_secret',
1573+
'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING,
1574+
'interface' => '2fa-secret',
1575+
'locked' => 1,
1576+
'readonly' => 1
1577+
],
15701578

15711579
// User Roles Junction
15721580
// -----------------------------------------------------------------
@@ -1592,6 +1600,12 @@ public function run()
15921600
'type' => \Directus\Database\Schema\DataTypes::TYPE_M2O,
15931601
'interface' => 'many-to-one',
15941602
'locked' => 1
1603+
],
1604+
[
1605+
'collection' => 'directus_user_roles',
1606+
'field' => 'enforce_2fa',
1607+
'type' => \Directus\Database\Schema\DataTypes::TYPE_BOOLEAN,
1608+
'interface' => 'toggle'
15951609
]
15961610
];
15971611

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use Phinx\Migration\AbstractMigration;
4+
5+
class AddUsers2FASecretField extends AbstractMigration
6+
{
7+
public function change()
8+
{
9+
$table = $this->table('directus_users');
10+
if (!$table->hasColumn('2fa_secret')) {
11+
$table->addColumn('2fa_secret', 'string', [
12+
'limit' => 255,
13+
'null' => true,
14+
'default' => null
15+
]);
16+
17+
$table->save();
18+
}
19+
20+
$collection = 'directus_users';
21+
$field = '2fa_secret';
22+
$checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field);
23+
$result = $this->query($checkSql)->fetch();
24+
25+
if (!$result) {
26+
$insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`, `readonly`, `hidden_detail`, `hidden_browse`) VALUES ("%s", "%s", "%s", "%s", "%s", "%s", "%s");';
27+
$insertSql = sprintf($insertSqlFormat, $collection, $field, 'string', '2fa-secret', 1, 0, 1);
28+
$this->execute($insertSql);
29+
}
30+
}
31+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
4+
use Phinx\Migration\AbstractMigration;
5+
6+
class AddEnforce2FARoleField extends AbstractMigration
7+
{
8+
public function up()
9+
{
10+
$this->addSetting();
11+
$this->addField();
12+
}
13+
14+
protected function addSetting()
15+
{
16+
$table = $this->table('directus_roles');
17+
if (!$table->hasColumn('enforce_2fa')) {
18+
$table->addColumn('enforce_2fa', 'boolean', [
19+
'null' => true,
20+
'default' => null
21+
]);
22+
23+
$table->save();
24+
}
25+
}
26+
27+
protected function addField()
28+
{
29+
$collection = 'directus_roles';
30+
$field = 'enforce_2fa';
31+
$checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field);
32+
$result = $this->query($checkSql)->fetch();
33+
34+
if (!$result) {
35+
$insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`) VALUES ("%s", "%s", "%s", "%s");';
36+
$insertSql = sprintf($insertSqlFormat, $collection, $field, 'boolean', 'toggle');
37+
$this->execute($insertSql);
38+
}
39+
}
40+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
4+
use Phinx\Migration\AbstractMigration;
5+
6+
class UpdateDirectusFieldsField extends AbstractMigration
7+
{
8+
public function up()
9+
{
10+
$this->execute(\Directus\phinx_update(
11+
$this->getAdapter(),
12+
'directus_fields',
13+
[
14+
'readonly' => 0,
15+
'note' => 'Duration must be in seconds'
16+
],
17+
['collection' => 'directus_files', 'field' => 'duration']
18+
));
19+
20+
21+
}
22+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@directus/api",
33
"private": true,
4-
"version": "2.4.0",
4+
"version": "2.5.0",
55
"description": "Directus API",
66
"main": "index.js",
77
"repository": "directus/api",

src/core/Directus/Application/Application.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Application extends App
1313
*
1414
* @var string
1515
*/
16-
const DIRECTUS_VERSION = '2.4.0';
16+
const DIRECTUS_VERSION = '2.5.0';
1717

1818
/**
1919
* NOT USED

src/core/Directus/Application/CoreServicesProvider.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,11 @@ protected function getEmitter()
273273

274274

275275
if ($dateCreated = $collection->getDateCreatedField()) {
276-
$payload[$dateCreated->getName()] = DateTimeUtils::nowInUTC()->toString();
276+
$payload[$dateCreated->getName()] = DateTimeUtils::nowInTimezone()->toString();
277277
}
278278

279279
if ($dateModified = $collection->getDateModifiedField()) {
280-
$payload[$dateModified->getName()] = DateTimeUtils::nowInUTC()->toString();
280+
$payload[$dateModified->getName()] = DateTimeUtils::nowInTimezone()->toString();
281281
}
282282

283283
// Directus Users created user are themselves (primary key)
@@ -364,7 +364,7 @@ protected function getEmitter()
364364
/** @var Acl $acl */
365365
$acl = $container->get('acl');
366366
if ($dateModified = $collection->getDateModifiedField()) {
367-
$payload[$dateModified->getName()] = DateTimeUtils::nowInUTC()->toString();
367+
$payload[$dateModified->getName()] = DateTimeUtils::nowInTimezone()->toString();
368368
}
369369

370370
if ($userModified = $collection->getUserModifiedField()) {

src/core/Directus/Application/Http/Middleware/AuthenticationMiddleware.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Directus\Application\Http\Request;
66
use Directus\Application\Http\Response;
7+
use Directus\Authentication\Exception\TFAEnforcedException;
78
use Directus\Authentication\Exception\UserNotAuthenticatedException;
89
use Directus\Authentication\User\User;
910
use Directus\Authentication\User\UserInterface;
@@ -12,6 +13,7 @@
1213
use function Directus\get_request_authorization_token;
1314
use Directus\Permissions\Acl;
1415
use Directus\Services\AuthService;
16+
use Directus\Services\UsersService;
1517
use Zend\Db\Sql\Select;
1618
use Zend\Db\TableGateway\TableGateway;
1719

@@ -26,6 +28,7 @@ class AuthenticationMiddleware extends AbstractMiddleware
2628
*
2729
* @throws UnauthorizedLocationException
2830
* @throws UserNotAuthenticatedException
31+
* @throws TFAEnforcedException
2932
*/
3033
public function __invoke(Request $request, Response $response, callable $next)
3134
{
@@ -53,7 +56,19 @@ public function __invoke(Request $request, Response $response, callable $next)
5356

5457
if (!is_null($user)) {
5558
$rolesIpWhitelist = $this->getUserRolesIPWhitelist($user->getId());
56-
$permissionsByCollection = $permissionsTable->getUserPermissions($user->getId());
59+
$permissionsByCollection = $permissionsTable->getUserPermissions($user->getId());
60+
61+
/** @var UsersService $usersService */
62+
$usersService = new UsersService($this->container);
63+
$tfa_enforced = $usersService->has2FAEnforced($user->getId());
64+
$isUserEdit = $this->targetIsUserEdit($request, $user->getId());
65+
66+
if ($tfa_enforced && $user->get2FASecret() == null && !$isUserEdit) {
67+
$exception = new TFAEnforcedException();
68+
$hookEmitter->run('auth.fail', [$exception]);
69+
throw $exception;
70+
}
71+
5772
$hookEmitter->run('auth.success', [$user]);
5873
} else {
5974
if (is_null($user) && $publicRoleId) {
@@ -207,4 +222,32 @@ protected function getRoleIPWhitelist($roleId)
207222

208223
return array_filter(preg_split('/,\s*/', $result['ip_whitelist']));
209224
}
225+
226+
/**
227+
* Returns true if the request is a user update for the given id
228+
* A user edit will submit a PATCH to both the user update endpoint
229+
*
230+
* @param Request $request
231+
* @param int $id
232+
*
233+
* @return bool
234+
*/
235+
protected function targetIsUserEdit(Request $request, int $id) {
236+
237+
$target_array = explode('/', $request->getRequestTarget());
238+
$num_elements = count($target_array);
239+
240+
if (!$request->isPost()) {
241+
return false;
242+
}
243+
244+
if ($num_elements > 3
245+
&&$target_array[$num_elements - 3] == 'users'
246+
&& $target_array[$num_elements - 2] == strval($id)
247+
&& $target_array[$num_elements - 1] == 'activate2FA') {
248+
return true;
249+
}
250+
251+
return false;
252+
}
210253
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Directus\Authentication\Exception;
4+
5+
use Directus\Exception\NotFoundException;
6+
7+
class InvalidOTPException extends NotFoundException
8+
{
9+
const ERROR_CODE = 112;
10+
11+
public function __construct()
12+
{
13+
parent::__construct('Invalid user OTP', static::ERROR_CODE);
14+
}
15+
}

0 commit comments

Comments
 (0)