'required|between:6,255|email|unique:backend_users', 'login' => 'required|between:2,255|unique:backend_users', 'password' => 'required:create|min:4|confirmed', 'password_confirmation' => 'required_with:password|min:4' ]; /** * @var array Attributes that should be cast to dates */ protected $dates = [ 'activated_at', 'last_login', 'created_at', 'updated_at', 'deleted_at', ]; /** * Relations */ public $belongsToMany = [ 'groups' => [UserGroup::class, 'table' => 'backend_users_groups', 'softDelete' => true] ]; public $belongsTo = [ 'role' => UserRole::class ]; public $attachOne = [ 'avatar' => \System\Models\File::class ]; public $hasMany = [ 'throttle' => [UserThrottle::class, 'key' => 'user_id'] ]; /** * Purge attributes from data set. */ protected $purgeable = ['password_confirmation', 'send_invite']; /** * @var array List of attribute names which are json encoded and decoded from the database. */ protected $jsonable = ['permissions', 'metadata']; /** * @var string Login attribute */ public static $loginAttribute = 'login'; /** * @var array Relations on this model that require `backend.manage_users` * to change on another user's record. Deliberately limited to the relations this * model owns: authorization semantics for plugin-added relations belong to the * plugin, which can append to this list or bind its own guard to the * `model.relation.*` events. */ public array $permissionGuardedRelations = ['groups', 'avatar', 'throttle']; public function __construct(array $attributes = []) { parent::__construct($attributes); // Guard relation writes at the point they actually happen: direct // attach()/detach(), deferred binding commits and queued relation // syncs all funnel through these relation events. $guard = function (string $relationName) { $this->authorizeRelationChange($relationName); }; $this->bindEvent('model.relation.beforeAttach', $guard); $this->bindEvent('model.relation.beforeDetach', $guard); $this->bindEvent('model.relation.beforeAdd', $guard); $this->bindEvent('model.relation.beforeRemove', $guard); } /** * @return string Returns the user's full name. */ public function getFullNameAttribute() { return trim($this->first_name . ' ' . $this->last_name); } /** * Gets a code for when the user is persisted to a cookie or session which identifies the user. * @return string */ public function getPersistCode() { // Option A: @todo config // return parent::getPersistCode(); // Option B: if (!$this->persist_code) { return parent::getPersistCode(); } return $this->persist_code; } /** * Returns the public image file path to this user's avatar. */ public function getAvatarThumb($size = 25, $options = null) { if (is_string($options)) { $options = ['default' => $options]; } elseif (!is_array($options)) { $options = []; } // Default is "mm" (Mystery man) $default = array_get($options, 'default', 'mm'); if ($this->avatar) { return $this->avatar->getThumb($size, $size, $options); } return '//www.gravatar.com/avatar/' . md5(strtolower(trim($this->email))) . '?s='. $size . '&d='. urlencode($default); } /** * Determine whether the given user (or the currently authenticated user) * is authorized to manage this user record. * * Returns true when no user is provided and no user is authenticated (CLI/queue), * or when the user has `backend.manage_users` and (if this record is a superuser) * the user is also a superuser. */ public function canBeManagedByUser(?User $user = null): bool { $user = $user ?? BackendAuth::getUser(); if (!$user) { return true; } if (!$user->hasAccess('backend.manage_users')) { return false; } if (!$user->isSuperUser() && ($this->is_superuser || $this->getOriginal('is_superuser'))) { return false; } return true; } /** * Before create event — enforce authorization rules to prevent privilege escalation. */ public function beforeCreate() { $this->authorizeChange(); } /** * Before update event — enforce authorization rules to prevent privilege escalation. * * Bound to update rather than save so that a save with nothing to write (e.g. a * pivot form submission re-saving an untouched related record) requires no * permission: the update event only fires when attributes have actually changed, * after purgeable attributes have been stripped. Relation writes (group * membership, avatar) are guarded separately by authorizeRelationChange(). */ public function beforeUpdate() { $this->authorizeChange(); } /** * Enforce authorization rules for a change to this record's attributes. * * @throws AuthorizationException if the current user lacks permission */ protected function authorizeChange(): void { $actor = BackendAuth::getUser(); if (!$actor) { return; } $isCurrentUser = $this->exists && $actor->getKey() === $this->getKey(); if ($isCurrentUser && $this->isDirty(['role_id', 'is_superuser', 'permissions'])) { throw new AuthorizationException(Lang::get('backend::lang.user.self_escalation_denied')); } if (!$isCurrentUser && !$this->canBeManagedByUser($actor)) { throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user')); } } /** * Enforce authorization rules for a change to one of this record's relations. * * Only the relations listed in $permissionGuardedRelations are guarded, so * plugin-added relations (e.g. records that reference a user) behave normally. * * Changes to your own record's guarded relations are allowed: groups do not * carry permissions out of the box, so they are not an escalation vector. * * @throws AuthorizationException if the current user lacks permission */ protected function authorizeRelationChange(string $relationName): void { if (!in_array($relationName, $this->permissionGuardedRelations)) { return; } $actor = BackendAuth::getUser(); if (!$actor) { return; } $isCurrentUser = $this->exists && $actor->getKey() === $this->getKey(); if (!$isCurrentUser && !$this->canBeManagedByUser($actor)) { throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user')); } } /** * Before delete event — enforce authorization rules. */ public function beforeDelete() { if (!$this->canBeManagedByUser()) { throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user')); } } /** * Before restore event — enforce authorization rules. */ public function beforeRestore() { if (!$this->canBeManagedByUser()) { throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user')); } } /** * After create event * @return void */ public function afterCreate() { $this->restorePurgedValues(); if ($this->send_invite) { $this->sendInvitation(); } } /** * After login event * @return void */ public function afterLogin() { parent::afterLogin(); /** * @event backend.user.login * Provides an opportunity to interact with the Backend User model after the user has logged in * * Example usage: * * Event::listen('backend.user.login', function ((\Backend\Models\User) $user) { * Flash::success(sprintf('Welcome %s!', $user->getFullNameAttribute())); * }); * */ Event::fire('backend.user.login', [$this]); } /** * Sends an invitation to the user using template "backend::mail.invite". * @return void */ public function sendInvitation() { $data = [ 'name' => $this->full_name, 'login' => $this->login, 'password' => $this->getOriginalHashValue('password'), 'link' => Backend::url('backend'), ]; Mail::send('backend::mail.invite', $data, function ($message) { $message->to($this->email, $this->full_name); }); } public function getGroupsOptions() { $result = []; foreach (UserGroup::all() as $group) { $result[$group->id] = [$group->name, $group->description]; } return $result; } public function getRoleOptions() { $result = []; foreach (UserRole::all() as $role) { $result[$role->id] = [$role->name, $role->description]; } return $result; } /** * Check if the user is suspended. * @return bool */ public function isSuspended() { return BackendAuth::findThrottleByUserId($this->id)->checkSuspended(); } /** * Remove the suspension on this user. * * @throws AuthorizationException if the current user lacks permission */ public function unsuspend() { if (!$this->canBeManagedByUser()) { throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user')); } BackendAuth::findThrottleByUserId($this->id)->unsuspend(); } /** * Get a reset password code for this user. * * When called by an authenticated user targeting a different account, * the actor must have `backend.manage_users` permission. * Self-service resets (Auth controller restore flow) are allowed. * * @throws AuthorizationException if the current user lacks permission */ public function getResetPasswordCode() { $actor = BackendAuth::getUser(); if ($actor && $actor->getKey() !== $this->getKey() && !$this->canBeManagedByUser($actor)) { throw new AuthorizationException(Lang::get('backend::lang.user.cannot_manage_user')); } return parent::getResetPasswordCode(); } // // Impersonation // /** * Returns an array of merged permissions based on the user's individual permissions * and their role permissions filtering out any permissions the impersonator doesn't * have access to (if the current user is being impersonated) * * @return array */ public function getMergedPermissions() { if (!$this->mergedPermissions) { $permissions = parent::getMergedPermissions(); // If the user is being impersonated filter out any permissions the impersonator doesn't have access to already if (BackendAuth::isImpersonator()) { $impersonator = BackendAuth::getImpersonator(); if ($impersonator && $impersonator !== $this) { foreach ($permissions as $permission => $status) { if (!$impersonator->hasAccess($permission)) { unset($permissions[$permission]); } } $this->mergedPermissions = $permissions; } } } return $this->mergedPermissions; } /** * Check if this user can be impersonated by the provided impersonator * Super users cannot be impersonated and all users cannot be impersonated unless there is an impersonator * present and the impersonator has access to `backend.impersonate_users`, and the impersonator is not the * user being impersonated * * @param \Winter\Storm\Auth\Models\User|false $impersonator The user attempting to impersonate this user, false when not available * @return boolean */ public function canBeImpersonated($impersonator = false) { if ( $this->isSuperUser() || !$impersonator || !($impersonator instanceof static) || !$impersonator->hasAccess('backend.impersonate_users') || $impersonator === $this ) { return false; } // Clear the merged permissions before the impersonation starts // so that they are correct even if they had been loaded prior // to the impersonation starting $this->mergedPermissions = null; return true; } }