diff --git a/lib/User/Bootstrap.php b/lib/User/Bootstrap.php index 3ee7d56..db455bf 100644 --- a/lib/User/Bootstrap.php +++ b/lib/User/Bootstrap.php @@ -1,6 +1,8 @@ hasModule('user') && $app->getModule('user') instanceof Module) { $map = $this->buildClassMap($app->getModule('user')->classMap); - $this->initContainer($app,$map); + $this->initContainer($app, $map); $this->initTranslations($app); $this->initMailServiceConfiguration($app, $app->getModule('user')); @@ -30,6 +32,7 @@ class Bootstrap implements BootstrapInterface $this->initControllerNamespace($app); $this->initUrlRoutes($app); $this->initAuthCollection($app); + $this->initAuthManager($app); } else { /** @var $app ConsoleApplication */ $this->initConsoleCommands($app); @@ -66,6 +69,7 @@ class Bootstrap implements BootstrapInterface $di->set(Helper\AuthHelper::class); $di->set(Helper\GravatarHelper::class); $di->set(Helper\SecurityHelper::class); + $di->set(Helper\TimezoneHelper::class); // services $di->set(Service\AccountConfirmationService::class); @@ -143,6 +147,23 @@ class Bootstrap implements BootstrapInterface } } + /** + * Ensures the auth manager is the one provided by the library. + * + * @param Application $app + */ + protected function initAuthModule(Application $app) + { + if (!($app->getAuthManager() instanceof AuthManagerInterface)) { + $app->set( + 'authManager', + [ + 'class' => AuthDbManagerComponent::class + ] + ); + } + } + /** * Initializes web url routes (rules in Yii2) * @@ -170,7 +191,7 @@ class Bootstrap implements BootstrapInterface * Ensures required mail parameters needed for the mail service. * * @param Application $app - * @param Module $module + * @param Module|\yii\base\Module $module */ protected function initMailServiceConfiguration(Application $app, Module $module) { diff --git a/lib/User/Component/AuthDbManagerComponent.php b/lib/User/Component/AuthDbManagerComponent.php new file mode 100644 index 0000000..a546ef8 --- /dev/null +++ b/lib/User/Component/AuthDbManagerComponent.php @@ -0,0 +1,75 @@ +from($this->itemTable); + + if ($type !== null) { + $query->where(['type' => $type]); + } else { + $query->orderBy('type'); + } + + foreach ($excludeItems as $name) { + $query->andWhere('name <> :item', ['item' => $name]); + } + + $items = []; + + foreach ($query->all($this->db) as $row) { + $items[$row['name']] = $this->populateItem($row); + } + + return $items; + } + + /** + * Returns both roles and permissions assigned to user. + * + * @param integer $userId + * + * @return array + */ + public function getItemsByUser($userId) + { + if (empty($userId)) { + return []; + } + + $query = (new Query()) + ->select('b.*') + ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable]) + ->where('{{a}}.[[item_name]]={{b}}.[[name]]') + ->andWhere(['a.user_id' => (string)$userId]); + + $roles = []; + foreach ($query->all($this->db) as $row) { + $roles[$row['name']] = $this->populateItem($row); + $roles[$row['name']] = $this->populateItem($row); + } + + return $roles; + } + + /** + * @inheritdoc + */ + public function getItem($name) + { + return parent::getItem($name); + } +} diff --git a/lib/User/Contracts/AuthManagerInterface.php b/lib/User/Contracts/AuthManagerInterface.php new file mode 100644 index 0000000..8fc5562 --- /dev/null +++ b/lib/User/Contracts/AuthManagerInterface.php @@ -0,0 +1,29 @@ +authHelper = $authHelper; + parent::__construct($id, $module, $config); + } + + /** + * @inheritdoc + */ + public function behaviors() + { + return [ + 'access' => [ + 'class' => AccessControl::className(), + 'ruleConfig' => [ + 'class' => AccessRuleFilter::className(), + ], + 'rules' => [ + [ + 'allow' => true, + 'roles' => ['admin'], + ], + ], + ], + ]; + } + + public function actionIndex() + { + $searchModel = $this->make($this->getSearchModelClass()); + + return $this->render( + 'index', + [ + 'searchModel' => $searchModel, + 'dataProvider' => $searchModel->search(Yii::$app->request->get()) + ] + ); + } + + public function actionCreate() + { + /** @var AbstractAuthItem $model */ + $model = $this->make($this->getModelClass(), [], ['scenario' => 'create']); + + $this->make(AjaxRequestModelValidator::class, [$model])->validate(); + + if ($model->load(Yii::$app->request->post())) { + if ($this->make(AuthItemEditionService::class, [$model])->run()) { + Yii::$app + ->getSession() + ->setFlash('success', Yii::t('user', 'Authorization item successfully created.')); + + return $this->redirect(['index']); + + } else { + Yii::$app->getSession()->setFlash('danger', Yii::t('user', 'Unable to create authorization item.')); + } + } + + return $this->render( + 'create', + [ + 'model' => $model, + 'unassignedItems' => $this->authHelper->getUnassignedItems($model) + ] + ); + } + + public function actionUpdate($name) + { + $authItem = $this->getItem($name); + + /** @var AbstractAuthItem $model */ + $model = $this->make($this->getModelClass(), [], ['scenario' => 'update', 'item' => $authItem]); + + $this->make(AjaxRequestModelValidator::class, [$model])->validate(); + + if ($model->load(Yii::$app->request->post())) { + + if ($this->make(AuthItemEditionService::class, [$model])->run()) { + Yii::$app + ->getSession() + ->setFlash('success', Yii::t('user', 'Authorization item successfully updated.')); + + return $this->redirect(['index']); + + } else { + Yii::$app->getSession()->setFlash('danger', Yii::t('user', 'Unable to update authorization item.')); + } + } + + return $this->render( + 'update', + [ + 'model' => $model, + 'unassignedItems' => $this->authHelper->getUnassignedItems($model) + ] + ); + } + + public function actionDelete($name) + { + $item = $this->getItem($name); + + if ($this->authHelper->remove($item)) { + Yii::$app->getSession()->setFlash('success', Yii::t('user', 'Authorization item successfully removed.')); + } else { + Yii::$app->getSession()->setFlash('success', Yii::t('user', 'Unable to remove authorization item.')); + } + + return $this->redirect(['index']); + } + + /** + * The fully qualified class name of the model + * + * @return string + */ + abstract protected function getModelClass(); + + /** + * The fully qualified class name of the search model + * + * @return string + */ + abstract protected function getSearchModelClass(); + + /** + * Returns the an auth item + * + * @param string $name + * + * @return \yii\rbac\Role|\yii\rbac\Permission|\yii\rbac\Rule + */ + abstract protected function getItem($name); + +} diff --git a/lib/User/Controller/AdminController.php b/lib/User/Controller/AdminController.php index b7932f5..2cfe63a 100644 --- a/lib/User/Controller/AdminController.php +++ b/lib/User/Controller/AdminController.php @@ -208,6 +208,7 @@ class AdminController extends Controller '_assignments', [ 'user' => $user, + 'params' => Yii::$app->request->post() ] ); } diff --git a/lib/User/Controller/PermissionController.php b/lib/User/Controller/PermissionController.php new file mode 100644 index 0000000..33d9524 --- /dev/null +++ b/lib/User/Controller/PermissionController.php @@ -0,0 +1,41 @@ +authHelper->getPermission($name); + + if ($authItem !== null) { + return $authItem; + } + + throw new NotFoundHttpException(); + } + +} diff --git a/lib/User/Controller/ProfileController.php b/lib/User/Controller/ProfileController.php index ddcf0bf..f0ede02 100644 --- a/lib/User/Controller/ProfileController.php +++ b/lib/User/Controller/ProfileController.php @@ -58,6 +58,7 @@ class ProfileController extends Controller public function actionShow($id) { $profile = $this->profileQuery->whereId($id)->one(); + if ($profile === null) { throw new NotFoundHttpException(); } diff --git a/lib/User/Controller/RoleController.php b/lib/User/Controller/RoleController.php new file mode 100644 index 0000000..b0812f2 --- /dev/null +++ b/lib/User/Controller/RoleController.php @@ -0,0 +1,40 @@ +authHelper->getRole($name); + + if ($authItem !== null) { + return $authItem; + } + + throw new NotFoundHttpException(); + } + +} diff --git a/lib/User/Factory/AuthItemFactory.php b/lib/User/Factory/AuthItemFactory.php new file mode 100644 index 0000000..e368a14 --- /dev/null +++ b/lib/User/Factory/AuthItemFactory.php @@ -0,0 +1,50 @@ + 'makeRole', + Item::TYPE_PERMISSION => 'makePermission' + ]; + + /** + * @param $name + * + * @return \yii\rbac\Permission + */ + public static function makePermission($name) + { + return Yii::$app->getAuthManager()->createPermission($name); + } + + /** + * @param $name + * + * @return \yii\rbac\Role + */ + public static function makeRole($name) + { + return Yii::$app->getAuthManager()->createRole($name); + } + + /** + * @param $type + * @param $name + * + * @return \yii\rbac\Role|\yii\rbac\Permission + * @throws Exception + */ + public static function makeByType($type, $name) + { + if (array_key_exists($type, self::$map)) { + return call_user_func([self::class, self::$map[$type]], $name); + } + + throw new Exception('Unknown strategy type'); + } +} diff --git a/lib/User/Helper/AuthHelper.php b/lib/User/Helper/AuthHelper.php index 5266c53..6b69d13 100644 --- a/lib/User/Helper/AuthHelper.php +++ b/lib/User/Helper/AuthHelper.php @@ -1,30 +1,31 @@ - */ class AuthHelper { + use AuthManagerTrait; + /** - * Checks whether + * Checks whether a user has certain role * + * @param $userId * @param $role * * @return bool */ public function hasRole($userId, $role) { - if (Yii::$app->getAuthManager()) { - $roles = array_keys(Yii::$app->getAuthManager()->getRolesByUser($userId)); + if ($this->getAuthManager()) { + $roles = array_keys($this->getAuthManager()->getRolesByUser($userId)); return in_array($role, $roles, true); } @@ -41,11 +42,61 @@ class AuthHelper { /** @var Module $module */ $module = Yii::$app->getModule('user'); - $hasAdministratorPermissionName = Yii::$app->getAuthManager() && $module->administratorPermissionName + $hasAdministratorPermissionName = $this->getAuthManager() && $module->administratorPermissionName ? Yii::$app->getUser()->can($module->administratorPermissionName) : false; return $hasAdministratorPermissionName || in_array($username, $module->administrators); } + /** + * @param $name + * + * @return null|\yii\rbac\Item|Permission + */ + public function getPermission($name) + { + return $this->getAuthManager()->getPermission($name); + } + + /** + * @param $name + * + * @return null|\yii\rbac\Item|Role + */ + public function getRole($name) + { + return $this->getAuthManager()->getRole($name); + } + + /** + * Removes a role, permission or rule from the RBAC system. + * + * @param Role|Permission|Rule $object + * + * @return bool whether the role, permission or rule is successfully removed + */ + public function remove($object) + { + return $this->getAuthManager()->remove($object); + } + + /** + * @param AbstractAuthItem $model + * + * @return array + */ + public function getUnassignedItems(AbstractAuthItem $model) + { + $excludeItems = $model->item !== null ? [$model->item->name] : []; + $items = $this->getAuthManager()->getItems($model->getType(), $excludeItems); + + return ArrayHelper::map( + $items, + 'name', + function ($item) { + return empty($item->description) ? $item->name : "{$item->name} ({$item->description})"; + } + ); + } } diff --git a/lib/User/Helper/TimezoneHelper.php b/lib/User/Helper/TimezoneHelper.php new file mode 100644 index 0000000..7a4c0ce --- /dev/null +++ b/lib/User/Helper/TimezoneHelper.php @@ -0,0 +1,35 @@ +getOffset() / 60 / 60; + $timeZones[] = [ + 'timezone' => $timeZone, + 'name' => "{$timeZone} (UTC " . ($offset > 0 ? '+' : '') . "{$offset})", + 'offset' => $offset + ]; + } + + ArrayHelper::multisort($timeZones, 'offset', SORT_DESC, SORT_NUMERIC); + + return $timeZones; + } +} diff --git a/lib/User/Model/AbstractAuthItem.php b/lib/User/Model/AbstractAuthItem.php new file mode 100644 index 0000000..5e7a810 --- /dev/null +++ b/lib/User/Model/AbstractAuthItem.php @@ -0,0 +1,120 @@ +item instanceof Item) { + $this->itemName = $this->item->name; + $this->name = $this->item->name; + $this->description = $this->item->description; + $this->children = array_keys($this->getAuthManager()->getChildren($this->item->name)); + if ($this->item->ruleName !== null) { + $this->rule = get_class($this->getAuthManager()->getRule($this->item->ruleName)); + } + } + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'name' => Yii::t('user', 'Name'), + 'description' => Yii::t('user', 'Description'), + 'children' => Yii::t('user', 'Children'), + 'rule' => Yii::t('user', 'Rule'), + ]; + } + + /** + * @inheritdoc + */ + public function scenarios() + { + return [ + 'create' => ['name', 'description', 'children', 'rule'], + 'update' => ['name', 'description', 'children', 'rule'], + ]; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['itemName', 'safe'], + ['name', 'required'], + ['name', 'match', 'pattern' => '/^[\w][\w-.:]+[\w]$/'], + [['name', 'description', 'rule'], 'trim'], + [ + 'name', + function () { + if ($this->getAuthManager()->getItem($this->name) !== null) { + $this->addError('name', Yii::t('user', 'Auth item with such name already exists')); + } + }, + 'when' => function () { + return $this->scenario == 'create' || $this->item->name != $this->name; + } + ], + ['children', RbacItemsValidator::class], + ['rule', RbacRuleValidator::class], + ]; + } + + /** + * @return bool + */ + public function getIsNewRecord() + { + return $this->item === null; + } + + /** + * @return Item + */ + abstract public function getType(); +} diff --git a/lib/User/Model/Assignment.php b/lib/User/Model/Assignment.php new file mode 100644 index 0000000..9904753 --- /dev/null +++ b/lib/User/Model/Assignment.php @@ -0,0 +1,54 @@ +user_id === null) { + throw new InvalidConfigException('"user_id" must be set.'); + } + + $this->items = array_keys($this->getAuthManager()->getItemsByUser($this->user_id)); + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'items' => Yii::t('user', 'Items') + ]; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['user_id', 'required'], + ['items', RbacItemsValidator::class], + ['user_id', 'integer'] + ]; + } +} diff --git a/lib/User/Model/Permission.php b/lib/User/Model/Permission.php new file mode 100644 index 0000000..9598489 --- /dev/null +++ b/lib/User/Model/Permission.php @@ -0,0 +1,12 @@ + ['name', 'description', 'rule_name'], + ]; + } + + public function search($params = []) + { + /** @var ArrayDataProvider $dataProvider */ + $dataProvider = $this->make(ArrayDataProvider::class); + + $query = (new Query()) + ->select(['name', 'description', 'rule_name']) + ->andWhere(['type' => $this->getType()]) + ->from($this->getAuthManager()->itemTable); + + if ($this->load($params) && $this->validate()) { + $query + ->andFilterWhere(['like', 'name', $this->name]) + ->andFilterWhere(['like', 'description', $this->description]) + ->andFilterWhere(['like', 'rule_name', $this->rule_name]); + } + + $dataProvider->allModels = $query->all($this->getAuthManager()->db); + + return $dataProvider; + + } +} diff --git a/lib/User/Search/PermissionSearch.php b/lib/User/Search/PermissionSearch.php new file mode 100644 index 0000000..c4f2efb --- /dev/null +++ b/lib/User/Search/PermissionSearch.php @@ -0,0 +1,16 @@ +model = $model; + } + + public function run() + { + if (!$this->model->validate()) { + return false; + } + try { + if ($this->model->getIsNewRecord()) { + $item = AuthItemFactory::makeByType($this->model->getType(), $this->model->name); + } else { + $item = $this->model->item; + } + + $item->name = $this->model->name; + $item->description = $this->model->description; + + if (!empty($this->model->rule)) { + $rule = $this->make($this->model->rule); + if (null === $this->getAuthManager()->getRule($rule->name)) { + $this->getAuthManager()->add($rule); + } + $item->ruleName = $rule->name; + } else { + $item->ruleName = null; + } + + if ($this->model->getIsNewRecord()) { + $this->getAuthManager()->add($item); + } else { + $this->getAuthManager()->update($this->model->itemName, $item); + $this->model->itemName = $item->name; + } + + $this->model->item = $item; + + return $this->updateChildren(); + + } catch (Exception $e) { + return false; + } + } + + /** + * Updates Auth Item children + * + * @return bool + */ + protected function updateChildren() + { + $children = $this->getAuthManager()->getChildren($this->model->item->name); + $childrenNames = array_keys($children); + + if (is_array($this->model->children)) { + + // remove those not linked anymore + foreach (array_diff($childrenNames, $this->model->children) as $item) { + if (!$this->getAuthManager()->removeChild($this->model->item, $children[$item])) { + return false; + } + + } + // add new children + foreach (array_diff($this->model->children, $childrenNames) as $item) { + if (!$this->getAuthManager()->addChild($this->model->item, $this->getAuthManager()->getItem($item))) { + return false; + } + } + } else { + return $this->getAuthManager()->removeChildren($this->model->item); + } + + return true; + } +} diff --git a/lib/User/Service/UpdateAuthAssignmentsService.php b/lib/User/Service/UpdateAuthAssignmentsService.php new file mode 100644 index 0000000..0cad148 --- /dev/null +++ b/lib/User/Service/UpdateAuthAssignmentsService.php @@ -0,0 +1,44 @@ +model = $model; + } + + public function run() + { + if ($this->model->validate()) { + return false; + } + + if (!is_array($this->model->items)) { + $this->model->items = []; + } + + $assignedItems = $this->getAuthManager()->getItemsByUser($this->model->user_id); + $assignedItemsNames = array_keys($assignedItems); + + foreach (array_diff($assignedItemsNames, $this->model->items) as $item) { + $this->model->getAuthManager()->revoke($assignedItems[$item], $this->model->user_id); + } + + foreach (array_diff($this->model->items, $assignedItemsNames) as $item) { + $this->getAuthManager()->assign($this->getAuthManager()->getItem($item), $this->model->user_id); + } + + return $this->model->updated = true; + + } +} diff --git a/lib/User/Traits/AuthManagerTrait.php b/lib/User/Traits/AuthManagerTrait.php new file mode 100644 index 0000000..5e041f9 --- /dev/null +++ b/lib/User/Traits/AuthManagerTrait.php @@ -0,0 +1,16 @@ +getAuthManager(); + } +} diff --git a/lib/User/Validator/RbacItemsValidator.php b/lib/User/Validator/RbacItemsValidator.php new file mode 100644 index 0000000..1960689 --- /dev/null +++ b/lib/User/Validator/RbacItemsValidator.php @@ -0,0 +1,25 @@ +getAuthManager()->getItem($item) == null) { + return [Yii::t('user', 'There is neither role nor permission with name "{0}"', [$item]), []]; + } + } + } + +} diff --git a/lib/User/Validator/RbacRuleValidator.php b/lib/User/Validator/RbacRuleValidator.php new file mode 100644 index 0000000..055bd2c --- /dev/null +++ b/lib/User/Validator/RbacRuleValidator.php @@ -0,0 +1,26 @@ +isInstantiable() == false) { + return [Yii::t('user', 'Rule class can not be instantiated'), []]; + } + if ($class->isSubclassOf('\yii\rbac\Rule') == false) { + return [Yii::t('user', 'Rule class must extend "yii\rbac\Rule"'), []]; + } + } +} diff --git a/lib/User/Widget/AssignmentsWidget.php b/lib/User/Widget/AssignmentsWidget.php index 16588c2..2d79f8d 100644 --- a/lib/User/Widget/AssignmentsWidget.php +++ b/lib/User/Widget/AssignmentsWidget.php @@ -2,44 +2,68 @@ namespace Da\User\Widget; -use dektrium\rbac\components\DbManager; -use dektrium\rbac\models\Assignment; -use Yii; +use Da\User\Model\Assignment; +use Da\User\Service\UpdateAuthAssignmentsService; +use Da\User\Traits\AuthManagerTrait; +use Da\User\Traits\ContainerTrait; use yii\base\InvalidConfigException; use yii\base\Widget; +use yii\helpers\ArrayHelper; class AssignmentsWidget extends Widget { - /** @var integer ID of the user to whom auth items will be assigned. */ + use AuthManagerTrait; + use ContainerTrait; + + /** + * @var integer ID of the user to whom auth items will be assigned. + */ public $userId; + /** + * @var string[] the post parameters + */ + public $params = []; - /** @var DbManager */ - protected $manager; - - /** @inheritdoc */ + /** + * @inheritdoc + * @throws InvalidConfigException + */ public function init() { parent::init(); - $this->manager = Yii::$app->authManager; if ($this->userId === null) { - throw new InvalidConfigException('You should set ' . __CLASS__ . '::$userId'); + throw new InvalidConfigException( __CLASS__ . '::$userId is required'); } } - /** @inheritdoc */ + /** + * @inheritdoc + */ public function run() { - $model = Yii::createObject([ - 'class' => Assignment::className(), - 'user_id' => $this->userId, - ]); + $model = $this->make(Assignment::class, [], ['user_id' => $this->userId]); - if ($model->load(\Yii::$app->request->post())) { - $model->updateAssignments(); + if ($model->load($this->params)) { + $this->make(UpdateAuthAssignmentsService::class, [$model])->run(); } return $this->render('/widgets/assignments/form', [ 'model' => $model, + 'availableItems' => $this->getAvailableItems() ]); } + + /** + * Returns all available auth items to be attached to the user + * + * @return array + */ + protected function getAvailableItems() + { + return ArrayHelper::map($this->getAuthManager()->getItems(), 'name', function ($item) { + return empty($item->description) + ? $item->name + : $item->name . ' (' . $item->description . ')'; + }); + } } diff --git a/lib/User/resources/views/admin/_assignments.php b/lib/User/resources/views/admin/_assignments.php index 1c09fda..99dd900 100644 --- a/lib/User/resources/views/admin/_assignments.php +++ b/lib/User/resources/views/admin/_assignments.php @@ -5,6 +5,7 @@ use Da\User\Widget\AssignmentsWidget; /** * @var yii\web\View $this * @var \Da\User\Model\User $user + * @var string[] $params */ ?> @@ -20,6 +21,6 @@ use Da\User\Widget\AssignmentsWidget; ] ) ?> -= AssignmentsWidget::widget(['userId' => $user->id]) ?> += AssignmentsWidget::widget(['userId' => $user->id, 'params' => $params]) ?> endContent() ?> diff --git a/lib/User/resources/views/admin/create.php b/lib/User/resources/views/admin/create.php index b91aa82..daf4779 100644 --- a/lib/User/resources/views/admin/create.php +++ b/lib/User/resources/views/admin/create.php @@ -14,7 +14,7 @@ $this->params['breadcrumbs'][] = ['label' => Yii::t('user', 'Users'), 'url' => [ $this->params['breadcrumbs'][] = $this->title; ?> - +
= $this->render( '/shared/_alert', [ @@ -22,69 +22,86 @@ $this->params['breadcrumbs'][] = $this->title; ] ) ?> -= $this->render('_menu') ?> -= Yii::t('user', 'You can connect multiple accounts to be able to log in using them') ?>.
| = $auth->isConnected($client) ? - Html::a(Yii::t('user', 'Disconnect'), $auth->createClientUrl($client), [ - 'class' => 'btn btn-danger btn-block', - 'data-method' => 'post', - ]) : - Html::a(Yii::t('user', 'Connect'), $auth->createClientUrl($client), [ - 'class' => 'btn btn-success btn-block', - ]) + Html::a( + Yii::t('user', 'Disconnect'), + $auth->createClientUrl($client), + [ + 'class' => 'btn btn-danger btn-block', + 'data-method' => 'post', + ] + ) : + Html::a( + Yii::t('user', 'Connect'), + $auth->createClientUrl($client), + [ + 'class' => 'btn btn-success btn-block', + ] + ) ?> |