Merge pull request #477 from MatteoF96/master

Implemented REST interface for admin controller
This commit is contained in:
Lorenzo Milesi
2022-09-23 12:44:15 +02:00
committed by GitHub
8 changed files with 559 additions and 2 deletions

3
.gitignore vendored
View File

@ -52,3 +52,6 @@ codeception.yml
# composer
composer.lock
# visual studio code
.vscode

View File

@ -38,6 +38,7 @@ There's a change in flash messages handling, please see #391
- Enh #472: implement module viewPath in all views instead of static file reference (tonisormisson)
- Fix: Clear 2FA auth key when feature is disabled by user
- Fix: check user before accessing 2FA code
- Enh: added `AdminController` REST controller (MatteoF96)
## 1.5.1 April 5, 2020

View File

@ -317,4 +317,41 @@ Possible array keys:
- special: minimum number of special characters;
- min: minimum number of characters (= minimum length).
#### enableRestApi (type: `boolean`, default: `false`)
Whether to enable REST APIs.
#### authenticatorClass (type: `string`, default: `yii\filters\auth\QueryParamAuth`)
Which class to use as authenticator for REST API.
Possible values ([official documentation](https://www.yiiframework.com/doc/guide/2.0/en/rest-authentication)):
- `HttpBasicAuth`
- `HttpBearerAuth`
- `QueryParamAuth`.
Default value = `yii\filters\auth\QueryParamAuth` class, therefore access tokens are sent as query parameter; for instance: `https://example.com/users?access-token=xxxxxxxx`.
#### adminRestPrefix (type: `string`, default: `user/api/v1`)
Route prefix for REST admin controller.
#### adminRestRoutes (type `array`)
Routes for REST admin controller.
Default value:
```php
[
'GET,HEAD users' => 'admin/index',
'POST users' => 'admin/create',
'PUT,PATCH users/<id>' => 'admin/update',
'GET,HEAD users/<id>' => 'admin/view',
'DELETE users/<id>' => 'admin/delete',
'users/<action>/<id>' => 'admin/<action>',
'users/<id>' => 'admin/options',
'users' => 'admin/options',
];
```
© [2amigos](http://www.2amigos.us/) 2013-2019

View File

@ -53,6 +53,7 @@ class Bootstrap implements BootstrapInterface
if ($app instanceof WebApplication) {
$this->initControllerNamespace($app);
$this->initUrlRoutes($app);
$this->initUrlRestRoutes($app);
$this->initAuthCollection($app);
$this->initAuthManager($app);
} else {
@ -277,6 +278,25 @@ class Bootstrap implements BootstrapInterface
$app->getUrlManager()->addRules([$rule], false);
}
/**
* Initializes web url for rest routes.
* @param WebApplication $app
* @throws InvalidConfigException
*/
protected function initUrlRestRoutes(WebApplication $app)
{
/** @var Module $module */
$module = $app->getModule('user');
$rules = $module->adminRestRoutes;
$config = [
'class' => 'yii\web\GroupUrlRule',
'prefix' => $module->adminRestPrefix,
'rules' => $rules,
];
$rule = Yii::createObject($config);
$app->getUrlManager()->addRules([$rule], false);
}
/**
* Ensures required mail parameters needed for the mail service.
*

View File

@ -0,0 +1,454 @@
<?php
namespace Da\User\Controller\api\v1;
use Da\User\Event\UserEvent;
use Da\User\Factory\MailFactory;
use Da\User\Model\Assignment;
use Da\User\Model\Profile;
use Da\User\Model\User;
use Da\User\Query\UserQuery;
use Da\User\Service\PasswordExpireService;
use Da\User\Service\PasswordRecoveryService;
use Da\User\Service\UserBlockService;
use Da\User\Service\UserConfirmationService;
use Da\User\Service\UserCreateService;
use Da\User\Traits\ContainerAwareTrait;
use Yii;
use yii\base\Module;
use yii\db\ActiveRecord;
use yii\filters\Cors;
use yii\rest\ActiveController;
use yii\web\BadRequestHttpException;
use yii\web\ForbiddenHttpException;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
/**
* Controller that provides REST APIs to manage users.
* This controller is equivalent to `Da\User\Controller\AdminController`.
*
* TODO:
* - `Info` and `SwitchIdentity` actions were not developed yet.
* - `Assignments` action implements only GET method (POST method not developed yet).
*/
class AdminController extends ActiveController
{
use ContainerAwareTrait;
/**
* {@inheritdoc}
*/
public $modelClass = 'Da\User\Model\User';
/**
* {@inheritdoc}
*/
public $updateScenario = 'update';
/**
* {@inheritdoc}
*/
public $createScenario = 'create';
/**
* @var UserQuery
*/
protected $userQuery;
/**
* AdminController constructor.
* @param string $id
* @param Module $module
* @param UserQuery $userQuery
* @param array $config
*/
public function __construct($id, Module $module, UserQuery $userQuery, array $config = [])
{
$this->userQuery = $userQuery;
parent::__construct($id, $module, $config);
}
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
// Set user properties for REST APIs
\Yii::$app->user->enableSession = false;
\Yii::$app->user->loginUrl = null;
}
/**
* {@inheritdoc}
*/
public function actions()
{
// Get and then remove some default actions
$actions = parent::actions();
unset($actions['create']);
unset($actions['update']);
unset($actions['delete']);
return $actions;
}
/**
* {@inheritdoc}
*/
protected function verbs()
{
// Get parent verbs
$verbs = parent::verbs();
// Add new verbs and return
$verbs['update-profile'] = ['PUT', 'PATCH'];
$verbs['assignments'] = ['GET'];
$verbs['confirm'] = ['PUT', 'PATCH'];
$verbs['block'] = ['PUT', 'PATCH'];
$verbs['password-reset'] = ['PUT', 'PATCH'];
$verbs['force-password-change'] = ['PUT', 'PATCH'];
return $verbs;
}
/**
* {@inheritdoc}
*/
public function behaviors()
{
$behaviors = parent::behaviors();
// Remove the (default) authentication filter
unset($behaviors['authenticator']);
// Cors filter
$behaviors['corsFilter'] = [
'class' => Cors::class,
];
// Re-add authentication filter
$behaviors['authenticator'] = [
'class' => $this->module->authenticatorClass, // Class depends on the module parameter
'except' => ['options']
];
// Return
return $behaviors;
}
/**
* {@inheritdoc}
*/
public function checkAccess($action, $model = null, $params = [])
{
// Check if the REST APIs are enabled
if (!$this->module->enableRestApi) {
throw new NotFoundHttpException(Yii::t('usuario', 'The requested page does not exist.'));
}
// Access for admins only
if (!Yii::$app->user->can('admin')) {
throw new ForbiddenHttpException(Yii::t('usuario', 'User does not have sufficient permissions.'));
}
}
/**
* Create a user.
*/
public function actionCreate()
{
// Check access
$this->checkAccess($this->action);
// Create new user model
/** @var User $user */
$user = $this->make(User::class, [], ['scenario' => $this->createScenario]);
// Create event object
/** @var UserEvent $event */
$event = $this->make(UserEvent::class, [$user]);
// Save user model + response
$user->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($user->validate()) {
$this->trigger(UserEvent::EVENT_BEFORE_CREATE, $event);
$mailService = MailFactory::makeWelcomeMailerService($user); // Welcome email
if ($this->make(UserCreateService::class, [$user, $mailService])->run()) {
$this->trigger(UserEvent::EVENT_AFTER_CREATE, $event);
Yii::$app->getResponse()->setStatusCode(201); // 201 = Created
return $user;
}
}
if (!$user->hasErrors()) {
$this->throwServerError();
}
return $user;
}
/**
* Update a user.
* @param int $id ID of the user.
*/
public function actionUpdate($id)
{
// Check access
$this->checkAccess($this->action);
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
$user->setScenario($this->updateScenario);
// Create event object
/** @var UserEvent $event */
$event = $this->make(UserEvent::class, [$user]);
// Save user model + response
$user->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($user->validate()) {
$this->trigger(UserEvent::EVENT_BEFORE_ACCOUNT_UPDATE, $event);
if ($user->save()) {
$this->trigger(UserEvent::EVENT_AFTER_ACCOUNT_UPDATE, $event);
return $user;
}
}
if (!$user->hasErrors()) {
$this->throwServerError();
}
return $user;
}
/**
* Delete a user.
* @param int $id ID of the user.
*/
public function actionDelete($id)
{
// Check access
$this->checkAccess($this->action);
// Check ID parameter (whether own account)
if ((int)$id === Yii::$app->user->getId()) {
throw new BadRequestHttpException(Yii::t('usuario', 'You cannot remove your own account.'));
}
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Create event object
/** @var UserEvent $event */
$event = $this->make(UserEvent::class, [$user]);
// Detele user model + response
$this->trigger(ActiveRecord::EVENT_BEFORE_DELETE, $event);
if ($user->delete()) {
$this->trigger(ActiveRecord::EVENT_AFTER_DELETE, $event);
Yii::$app->getResponse()->setStatusCode(204); // 204 = No Content
}
else {
$this->throwServerError();
}
}
/**
* Update the user profile.
* @param int $id ID of the user.
*/
public function actionUpdateProfile($id)
{
// Check access
$this->checkAccess($this->action);
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Get profile model
/** @var Profile $profile */
$profile = $user->profile;
if ($profile === null) {
$profile = $this->make(Profile::class);
$profile->link('user', $user);
}
// Create event object
/** @var UserEvent $event */
$event = $this->make(UserEvent::class, [$user]);
// Save profile model + response
$profile->load(Yii::$app->getRequest()->getBodyParams(), '');
$this->trigger(UserEvent::EVENT_BEFORE_PROFILE_UPDATE, $event);
if ($profile->save() === false && !$profile->hasErrors()) {
$this->throwServerError();
}
$this->trigger(UserEvent::EVENT_AFTER_PROFILE_UPDATE, $event);
return $profile;
}
/**
* Get assignments of the specified user.
* @param int $id ID of the user.
*/
public function actionAssignments($id)
{
// Check access
$this->checkAccess($this->action);
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Get assignments + response
$assignments = $this->make(Assignment::class, [], ['user_id' => $user->id]);
return $assignments;
}
/**
* Confirm the user.
* @param int $id ID of the user.
*/
public function actionConfirm($id)
{
// Check access
$this->checkAccess($this->action);
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Create event object
/** @var UserEvent $event */
$event = $this->make(UserEvent::class, [$user]);
// Confirm user + response
$this->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION, $event);
if ($this->make(UserConfirmationService::class, [$user])->run() || $user->hasErrors()) {
$this->trigger(UserEvent::EVENT_AFTER_CONFIRMATION, $event);
return $user;
}
else {
$this->throwServerError();
}
}
/**
* Block and unblock the user.
* @param int $id ID of the user.
*/
public function actionBlock($id)
{
// Check access
$this->checkAccess($this->action);
// Check ID parameter (whether own account)
if ((int)$id === Yii::$app->user->getId()) {
throw new BadRequestHttpException(Yii::t('usuario', 'You cannot block your own account.'));
}
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Create event object
/** @var UserEvent $event */
$event = $this->make(UserEvent::class, [$user]);
// Block user + response
if ($this->make(UserBlockService::class, [$user, $event, $this])->run() || $user->hasErrors()) {
return $user;
}
else {
$this->throwServerError();
}
}
/**
* Reset password.
* @param int $id ID of the user.
*/
public function actionPasswordReset($id)
{
// Check access
$this->checkAccess($this->action);
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Confirm user + response
$mailService = MailFactory::makeRecoveryMailerService($user->email);
if ($this->make(PasswordRecoveryService::class, [$user->email, $mailService])->run()) {
return $user;
}
else {
$this->throwServerError();
}
}
/**
* Forces the user to change password at next login.
* @param int $id ID of the user.
*/
public function actionForcePasswordChange($id)
{
// Check access
$this->checkAccess($this->action);
// Get user model
/** @var User $user */
$user = $this->userQuery->where(['id' => $id])->one();
if (is_null($user)) { // Check user, so `$id` parameter
$this->throwUser404();
}
// Confirm user + response
if ($this->make(PasswordExpireService::class, [$user])->run()) {
return $user;
}
else {
$this->throwServerError();
}
}
/**
* Handle server error (with default Yii2 response).
* @return void
* @throws ServerErrorHttpException
*/
protected function throwServerError()
{
throw new ServerErrorHttpException('Failed to create the object for unknown reason.'); // Yii2 standard response for server errors
}
/**
* Handle 404 error for user (usually if the entered ID is not valid).
* @return void
* @throws NotFoundHttpException
*/
protected function throwUser404()
{
throw new NotFoundHttpException(Yii::t('usuario', 'User not found.'));
}
}

View File

@ -91,6 +91,16 @@ class User extends ActiveRecord implements IdentityInterface
return '{{%user}}';
}
/**
* {@inheritdoc}
*/
public function fields()
{
$fields = parent::fields();
unset($fields['auth_key'], $fields['password_hash']);
return $fields;
}
/**
* {@inheritdoc}
*/

View File

@ -233,7 +233,6 @@ class Module extends BaseModule
* @var boolean whether to disable IP logging into user table
*/
public $disableIpLogging = false;
/**
* @var array Minimum requirements when a new password is automatically generated.
* Array structure: `requirement => minimum number characters`.
@ -250,6 +249,33 @@ class Module extends BaseModule
'digit' => 1,
'upper' => 1,
];
/**
* @var boolean Whether to enable REST APIs.
*/
public $enableRestApi = false;
/**
* @var string Which class to use as authenticator for REST API.
* Possible values: `HttpBasicAuth`, `HttpBearerAuth` or `QueryParamAuth`.
* Default value = `yii\filters\auth\QueryParamAuth` class, therefore access tokens are sent as query parameter; for instance: `https://example.com/users?access-token=xxxxxxxx`.
*/
public $authenticatorClass = 'yii\filters\auth\QueryParamAuth';
/**
* @var string Route prefix for REST admin controller.
*/
public $adminRestPrefix = 'user/api/v1';
/**
* @var array Routes for REST admin controller.
*/
public $adminRestRoutes = [
'GET,HEAD users' => 'admin/index',
'POST users' => 'admin/create',
'PUT,PATCH users/<id>' => 'admin/update',
'GET,HEAD users/<id>' => 'admin/view',
'DELETE users/<id>' => 'admin/delete',
'users/<action>/<id>' => 'admin/<action>',
'users/<id>' => 'admin/options',
'users' => 'admin/options',
];
/**
* @return string with the hit to be used with the give consent checkbox

View File

@ -13,9 +13,11 @@ namespace Da\User\Service;
use Da\User\Contracts\ServiceInterface;
use Da\User\Controller\AdminController;
use Da\User\Controller\api\v1\AdminController as AdminControllerRest;
use Da\User\Event\UserEvent;
use Da\User\Helper\SecurityHelper;
use Da\User\Model\User;
use TypeError;
class UserBlockService implements ServiceInterface
{
@ -27,9 +29,13 @@ class UserBlockService implements ServiceInterface
public function __construct(
User $model,
UserEvent $event,
AdminController $controller,
$controller,
SecurityHelper $securityHelper
) {
if (!in_array(get_class($controller), [AdminController::class, AdminControllerRest::class])) {
throw new TypeError('Argument controller must be either of type '
. AdminController::class . ' or ' . AdminControllerRest::class . ', ' . get_class($controller) . ' given');
}
$this->model = $model;
$this->event = $event;
$this->controller = $controller;