Skip to content

Commit

Permalink
Release v2.5.0 (directus#1222)
Browse files Browse the repository at this point in the history
* Issue Fix directus#1180 (directus#1183)

* Issue fix directus#1191 (directus#1192)

* Issue fix directus#1196 (directus#1197)

* Add 2fa authentication (directus#1031)

* Parent + Nested validation changes (directus#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 directus#1194 (directus#1195)

* Issue Fix directus#1194

* Update comment

* Valildation issue of O2M/M2O at insertion (directus#1198)

* Fox directus#1201 (directus#1202)

* Fix directus#1203 (directus#1204)

* Update collections() method in types.php (directus#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 (directus#1210)

Adds in detection and parsing for youtu.be shorthand URLs.

* Add check for environment on bootstrap (directus#1215)

* Fix directus#1186 [Create new error code for invalid login entity] (directus#1218)

* Fix directus#1217 (Changing password over the CLI doesn't work) (directus#1220)

* Feature/audio video upload (directus#1214)

* added file meta data for audio/video

* updates as per PR feedback

* Fix directus#1207 [Permission denied issue  when using translation interface] (directus#1221)

* Bump version to v2.5.0
  • Loading branch information
rijkvanzanten authored Aug 27, 2019
1 parent 77a95be commit c698bf5
Show file tree
Hide file tree
Showing 34 changed files with 558 additions and 92 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-gd": "*",
"webonyx/graphql-php": "^0.13.0"
"webonyx/graphql-php": "^0.13.0",
"char0n/ffmpeg-php": "^3.0.0",
"pragmarx/google2fa": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.25",
Expand Down
14 changes: 14 additions & 0 deletions migrations/db/seeds/FieldsSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,14 @@ public function run()
'readonly' => 1,
'hidden_detail' => 1
],
[
'collection' => 'directus_users',
'field' => '2fa_secret',
'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING,
'interface' => '2fa-secret',
'locked' => 1,
'readonly' => 1
],

// User Roles Junction
// -----------------------------------------------------------------
Expand All @@ -1592,6 +1600,12 @@ public function run()
'type' => \Directus\Database\Schema\DataTypes::TYPE_M2O,
'interface' => 'many-to-one',
'locked' => 1
],
[
'collection' => 'directus_user_roles',
'field' => 'enforce_2fa',
'type' => \Directus\Database\Schema\DataTypes::TYPE_BOOLEAN,
'interface' => 'toggle'
]
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use Phinx\Migration\AbstractMigration;

class AddUsers2FASecretField extends AbstractMigration
{
public function change()
{
$table = $this->table('directus_users');
if (!$table->hasColumn('2fa_secret')) {
$table->addColumn('2fa_secret', 'string', [
'limit' => 255,
'null' => true,
'default' => null
]);

$table->save();
}

$collection = 'directus_users';
$field = '2fa_secret';
$checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field);
$result = $this->query($checkSql)->fetch();

if (!$result) {
$insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`, `readonly`, `hidden_detail`, `hidden_browse`) VALUES ("%s", "%s", "%s", "%s", "%s", "%s", "%s");';
$insertSql = sprintf($insertSqlFormat, $collection, $field, 'string', '2fa-secret', 1, 0, 1);
$this->execute($insertSql);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php


use Phinx\Migration\AbstractMigration;

class AddEnforce2FARoleField extends AbstractMigration
{
public function up()
{
$this->addSetting();
$this->addField();
}

protected function addSetting()
{
$table = $this->table('directus_roles');
if (!$table->hasColumn('enforce_2fa')) {
$table->addColumn('enforce_2fa', 'boolean', [
'null' => true,
'default' => null
]);

$table->save();
}
}

protected function addField()
{
$collection = 'directus_roles';
$field = 'enforce_2fa';
$checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field);
$result = $this->query($checkSql)->fetch();

if (!$result) {
$insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`) VALUES ("%s", "%s", "%s", "%s");';
$insertSql = sprintf($insertSqlFormat, $collection, $field, 'boolean', 'toggle');
$this->execute($insertSql);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php


use Phinx\Migration\AbstractMigration;

class UpdateDirectusFieldsField extends AbstractMigration
{
public function up()
{
$this->execute(\Directus\phinx_update(
$this->getAdapter(),
'directus_fields',
[
'readonly' => 0,
'note' => 'Duration must be in seconds'
],
['collection' => 'directus_files', 'field' => 'duration']
));


}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@directus/api",
"private": true,
"version": "2.4.0",
"version": "2.5.0",
"description": "Directus API",
"main": "index.js",
"repository": "directus/api",
Expand Down
2 changes: 1 addition & 1 deletion src/core/Directus/Application/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Application extends App
*
* @var string
*/
const DIRECTUS_VERSION = '2.4.0';
const DIRECTUS_VERSION = '2.5.0';

/**
* NOT USED
Expand Down
6 changes: 3 additions & 3 deletions src/core/Directus/Application/CoreServicesProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,11 @@ protected function getEmitter()


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

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

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

if ($userModified = $collection->getUserModifiedField()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Directus\Application\Http\Request;
use Directus\Application\Http\Response;
use Directus\Authentication\Exception\TFAEnforcedException;
use Directus\Authentication\Exception\UserNotAuthenticatedException;
use Directus\Authentication\User\User;
use Directus\Authentication\User\UserInterface;
Expand All @@ -12,6 +13,7 @@
use function Directus\get_request_authorization_token;
use Directus\Permissions\Acl;
use Directus\Services\AuthService;
use Directus\Services\UsersService;
use Zend\Db\Sql\Select;
use Zend\Db\TableGateway\TableGateway;

Expand All @@ -26,6 +28,7 @@ class AuthenticationMiddleware extends AbstractMiddleware
*
* @throws UnauthorizedLocationException
* @throws UserNotAuthenticatedException
* @throws TFAEnforcedException
*/
public function __invoke(Request $request, Response $response, callable $next)
{
Expand Down Expand Up @@ -53,7 +56,19 @@ public function __invoke(Request $request, Response $response, callable $next)

if (!is_null($user)) {
$rolesIpWhitelist = $this->getUserRolesIPWhitelist($user->getId());
$permissionsByCollection = $permissionsTable->getUserPermissions($user->getId());
$permissionsByCollection = $permissionsTable->getUserPermissions($user->getId());

/** @var UsersService $usersService */
$usersService = new UsersService($this->container);
$tfa_enforced = $usersService->has2FAEnforced($user->getId());
$isUserEdit = $this->targetIsUserEdit($request, $user->getId());

if ($tfa_enforced && $user->get2FASecret() == null && !$isUserEdit) {
$exception = new TFAEnforcedException();
$hookEmitter->run('auth.fail', [$exception]);
throw $exception;
}

$hookEmitter->run('auth.success', [$user]);
} else {
if (is_null($user) && $publicRoleId) {
Expand Down Expand Up @@ -207,4 +222,32 @@ protected function getRoleIPWhitelist($roleId)

return array_filter(preg_split('/,\s*/', $result['ip_whitelist']));
}

/**
* Returns true if the request is a user update for the given id
* A user edit will submit a PATCH to both the user update endpoint
*
* @param Request $request
* @param int $id
*
* @return bool
*/
protected function targetIsUserEdit(Request $request, int $id) {

$target_array = explode('/', $request->getRequestTarget());
$num_elements = count($target_array);

if (!$request->isPost()) {
return false;
}

if ($num_elements > 3
&&$target_array[$num_elements - 3] == 'users'
&& $target_array[$num_elements - 2] == strval($id)
&& $target_array[$num_elements - 1] == 'activate2FA') {
return true;
}

return false;
}
}
15 changes: 15 additions & 0 deletions src/core/Directus/Authentication/Exception/InvalidOTPException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Directus\Authentication\Exception;

use Directus\Exception\NotFoundException;

class InvalidOTPException extends NotFoundException
{
const ERROR_CODE = 112;

public function __construct()
{
parent::__construct('Invalid user OTP', static::ERROR_CODE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Directus\Authentication\Exception;

use Directus\Exception\NotFoundException;

class Missing2FAPasswordException extends NotFoundException
{
const ERROR_CODE = 111;

public function __construct()
{
parent::__construct('User missing 2FA OTP', static::ERROR_CODE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Directus\Authentication\Exception;

use Directus\Exception\UnauthorizedException;

class TFAEnforcedException extends UnauthorizedException
{
const ERROR_CODE = 113;

public function __construct()
{
parent::__construct('2FA enforced but not activated for user');
}
}
Loading

0 comments on commit c698bf5

Please sign in to comment.