├── .gitignore ├── README.md ├── composer.json ├── phpunit.xml └── src └── Jenssegers └── Mongodb └── Sentry ├── Group.php ├── Throttle.php └── User.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel MongoDB Sentry 2 | ====================== 3 | 4 | Because Sentry's models extends the original Eloquent model, it could not be used with MongoDB. This package includes models that extends `Jenssegers\Mongodb\Model` and thus support MongoDB. 5 | 6 | Installation 7 | ------------ 8 | 9 | Make sure you have [jenssegers\mongodb](https://github.com/jenssegers/Laravel-MongoDB) installed before you continue. 10 | 11 | Install using composer: 12 | 13 | composer require jenssegers/mongodb-sentry 14 | 15 | For instructions on Sentry, check out https://cartalyst.com/manual/sentry/installation/laravel-4 16 | 17 | Usage 18 | ----- 19 | 20 | To use the included MongoDB-enabled models, change the Sentry configuration model sections: 21 | 22 | ``` 23 | 'groups' => array( 24 | 25 | 'model' => 'Jenssegers\Mongodb\Sentry\Group', 26 | 27 | ), 28 | 29 | 'users' => array( 30 | 31 | 'model' => 'Jenssegers\Mongodb\Sentry\User', 32 | 33 | ), 34 | 35 | 'throttling' => array( 36 | 37 | 'model' => 'Jenssegers\Mongodb\Sentry\Throttle', 38 | 39 | ), 40 | ``` 41 | 42 | Or if you have a custom model, make sure it extends the correct model. 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jenssegers/mongodb-sentry", 3 | "description": "An extension for Laravel-MongoDB that lets you work with Sentry", 4 | "keywords": ["laravel","sentry","mongodb","mongo","authentication"], 5 | "authors": [ 6 | { 7 | "name": "Jens Segers", 8 | "homepage": "http://jenssegers.be" 9 | } 10 | ], 11 | "license" : "MIT", 12 | "require": { 13 | "php": ">=5.3.0", 14 | "cartalyst/sentry": "dev-feature/laravel-5", 15 | "jenssegers/mongodb": "*" 16 | }, 17 | "autoload": { 18 | "psr-0": { 19 | "Jenssegers\\Mongodb\\Sentry": "src/" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Jenssegers/Mongodb/Sentry/Group.php: -------------------------------------------------------------------------------- 1 | Remove. 47 | * 1 => Add. 48 | * 49 | * @var array 50 | */ 51 | protected $allowedPermissionsValues = array(0, 1); 52 | 53 | /** 54 | * The Eloquent user model. 55 | * 56 | * @var string 57 | */ 58 | protected static $userModel = 'Jenssegers\Mongodb\Sentry\User'; 59 | 60 | /** 61 | * The user groups pivot table name. 62 | * 63 | * @var string 64 | */ 65 | protected static $userGroupsPivot = 'users_groups'; 66 | 67 | /** 68 | * Returns the group's ID. 69 | * 70 | * @return mixed 71 | */ 72 | public function getId() 73 | { 74 | return $this->getKey(); 75 | } 76 | 77 | /** 78 | * Returns the group's name. 79 | * 80 | * @return string 81 | */ 82 | public function getName() 83 | { 84 | return $this->name; 85 | } 86 | 87 | /** 88 | * Returns permissions for the group. 89 | * 90 | * @return array 91 | */ 92 | public function getPermissions() 93 | { 94 | return $this->permissions; 95 | } 96 | 97 | /** 98 | * See if a group has access to the passed permission(s). 99 | * 100 | * If multiple permissions are passed, the group must 101 | * have access to all permissions passed through, unless the 102 | * "all" flag is set to false. 103 | * 104 | * @param string|array $permissions 105 | * @param bool $all 106 | * @return bool 107 | */ 108 | public function hasAccess($permissions, $all = true) 109 | { 110 | $groupPermissions = $this->getPermissions(); 111 | 112 | if ( ! is_array($permissions)) 113 | { 114 | $permissions = (array) $permissions; 115 | } 116 | 117 | foreach ($permissions as $permission) 118 | { 119 | // We will set a flag now for whether this permission was 120 | // matched at all. 121 | $matched = true; 122 | 123 | // Now, let's check if the permission ends in a wildcard "*" symbol. 124 | // If it does, we'll check through all the merged permissions to see 125 | // if a permission exists which matches the wildcard. 126 | if ((strlen($permission) > 1) and ends_with($permission, '*')) 127 | { 128 | $matched = false; 129 | 130 | foreach ($groupPermissions as $groupPermission => $value) 131 | { 132 | // Strip the '*' off the end of the permission. 133 | $checkPermission = substr($permission, 0, -1); 134 | 135 | // We will make sure that the merged permission does not 136 | // exactly match our permission, but starts with it. 137 | if ($checkPermission != $groupPermission and starts_with($groupPermission, $checkPermission) and $value == 1) 138 | { 139 | $matched = true; 140 | break; 141 | } 142 | } 143 | } 144 | 145 | // Now, let's check if the permission starts in a wildcard "*" symbol. 146 | // If it does, we'll check through all the merged permissions to see 147 | // if a permission exists which matches the wildcard. 148 | elseif ((strlen($permission) > 1) and starts_with($permission, '*')) 149 | { 150 | $matched = false; 151 | 152 | foreach ($groupPermissions as $groupPermission => $value) 153 | { 154 | // Strip the '*' off the start of the permission. 155 | $checkPermission = substr($permission, 1); 156 | 157 | // We will make sure that the merged permission does not 158 | // exactly match our permission, but ends with it. 159 | if ($checkPermission != $groupPermission and ends_with($groupPermission, $checkPermission) and $value == 1) 160 | { 161 | $matched = true; 162 | break; 163 | } 164 | } 165 | } 166 | 167 | else 168 | { 169 | $matched = false; 170 | 171 | foreach ($groupPermissions as $groupPermission => $value) 172 | { 173 | // This time check if the groupPermission ends in wildcard "*" symbol. 174 | if ((strlen($groupPermission) > 1) and ends_with($groupPermission, '*')) 175 | { 176 | $matched = false; 177 | 178 | // Strip the '*' off the end of the permission. 179 | $checkGroupPermission = substr($groupPermission, 0, -1); 180 | 181 | // We will make sure that the merged permission does not 182 | // exactly match our permission, but starts wtih it. 183 | if ($checkGroupPermission != $permission and starts_with($permission, $checkGroupPermission) and $value == 1) 184 | { 185 | $matched = true; 186 | break; 187 | } 188 | } 189 | 190 | // Otherwise, we'll fallback to standard permissions checking where 191 | // we match that permissions explicitly exist. 192 | elseif ($permission == $groupPermission and $groupPermissions[$permission] == 1) 193 | { 194 | $matched = true; 195 | break; 196 | } 197 | } 198 | } 199 | 200 | // Now, we will check if we have to match all 201 | // permissions or any permission and return 202 | // accordingly. 203 | if ($all === true and $matched === false) 204 | { 205 | return false; 206 | } 207 | elseif ($all === false and $matched === true) 208 | { 209 | return true; 210 | } 211 | } 212 | 213 | if ($all === false) 214 | { 215 | return false; 216 | } 217 | 218 | return true; 219 | } 220 | 221 | /** 222 | * Returns if the user has access to any of the 223 | * given permissions. 224 | * 225 | * @param array $permissions 226 | * @return bool 227 | */ 228 | public function hasAnyAccess(array $permissions) 229 | { 230 | return $this->hasAccess($permissions, false); 231 | } 232 | 233 | /** 234 | * Returns the relationship between groups and users. 235 | * 236 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 237 | */ 238 | public function users() 239 | { 240 | return $this->belongsToMany(static::$userModel, static::$userGroupsPivot); 241 | } 242 | 243 | /** 244 | * Set the Eloquent model to use for user relationships. 245 | * 246 | * @param string $model 247 | * @return void 248 | */ 249 | public static function setUserModel($model) 250 | { 251 | static::$userModel = $model; 252 | } 253 | 254 | /** 255 | * Set the user groups pivot table name. 256 | * 257 | * @param string $tableName 258 | * @return void 259 | */ 260 | public static function setUserGroupsPivot($tableName) 261 | { 262 | static::$userGroupsPivot = $tableName; 263 | } 264 | 265 | /** 266 | * Saves the group. 267 | * 268 | * @param array $options 269 | * @return bool 270 | */ 271 | public function save(array $options = array()) 272 | { 273 | $this->validate(); 274 | return parent::save(); 275 | } 276 | 277 | /** 278 | * Delete the group. 279 | * 280 | * @return bool 281 | */ 282 | public function delete() 283 | { 284 | $this->users()->detach(); 285 | return parent::delete(); 286 | } 287 | 288 | /** 289 | * Mutator for giving permissions. 290 | * 291 | * @param mixed $permissions 292 | * @return array 293 | * @throws \InvalidArgumentException 294 | */ 295 | public function getPermissionsAttribute($permissions) 296 | { 297 | if ( ! $permissions) 298 | { 299 | return array(); 300 | } 301 | 302 | if (is_array($permissions)) 303 | { 304 | return $permissions; 305 | } 306 | 307 | if ( ! $_permissions = json_decode($permissions, true)) 308 | { 309 | throw new \InvalidArgumentException("Cannot JSON decode permissions [$permissions]."); 310 | } 311 | 312 | return $_permissions; 313 | } 314 | 315 | /** 316 | * Mutator for taking permissions. 317 | * 318 | * @param array $permissions 319 | * @return void 320 | * @throws \InvalidArgumentException 321 | */ 322 | public function setPermissionsAttribute(array $permissions) 323 | { 324 | // Merge permissions 325 | $permissions = array_merge($this->getPermissions(), $permissions); 326 | 327 | // Loop through and adjust permissions as needed 328 | foreach ($permissions as $permission => &$value) 329 | { 330 | // Lets make sure their is a valid permission value 331 | if ( ! in_array($value = (int) $value, $this->allowedPermissionsValues)) 332 | { 333 | throw new \InvalidArgumentException("Invalid value [$value] for permission [$permission] given."); 334 | } 335 | 336 | // If the value is 0, delete it 337 | if ($value === 0) 338 | { 339 | unset($permissions[$permission]); 340 | } 341 | } 342 | 343 | $this->attributes['permissions'] = ( ! empty($permissions)) ? json_encode($permissions) : ''; 344 | } 345 | 346 | /** 347 | * Convert the model instance to an array. 348 | * 349 | * @return array 350 | */ 351 | public function toArray() 352 | { 353 | $attributes = parent::toArray(); 354 | 355 | if (isset($attributes['permissions'])) 356 | { 357 | $attributes['permissions'] = $this->getPermissionsAttribute($attributes['permissions']); 358 | } 359 | 360 | return $attributes; 361 | } 362 | 363 | /** 364 | * Validates the group and throws a number of 365 | * Exceptions if validation fails. 366 | * 367 | * @return bool 368 | * @throws \Cartalyst\Sentry\Groups\NameRequiredException 369 | * @throws \Cartalyst\Sentry\Groups\GroupExistsException 370 | */ 371 | public function validate() 372 | { 373 | // Check if name field was passed 374 | if ( ! $name = $this->name) 375 | { 376 | throw new NameRequiredException("A name is required for a group, none given."); 377 | } 378 | 379 | // Check if group already exists 380 | $query = $this->newQuery(); 381 | $persistedGroup = $query->where('name', '=', $name)->first(); 382 | 383 | if ($persistedGroup and $persistedGroup->getId() != $this->getId()) 384 | { 385 | throw new GroupExistsException("A group already exists with name [$name], names must be unique for groups."); 386 | } 387 | 388 | return true; 389 | } 390 | 391 | } 392 | -------------------------------------------------------------------------------- /src/Jenssegers/Mongodb/Sentry/Throttle.php: -------------------------------------------------------------------------------- 1 | user()->getResults(); 79 | } 80 | 81 | /** 82 | * Get the current amount of attempts. 83 | * 84 | * @return int 85 | */ 86 | public function getLoginAttempts() 87 | { 88 | if ($this->attempts > 0 and $this->last_attempt_at) 89 | { 90 | $this->clearLoginAttemptsIfAllowed(); 91 | } 92 | 93 | return $this->attempts; 94 | } 95 | 96 | /** 97 | * Get the number of login attempts a user has left before suspension. 98 | * 99 | * @return int 100 | */ 101 | public function getRemainingLoginAttempts() 102 | { 103 | return static::getAttemptLimit() - $this->getLoginAttempts(); 104 | } 105 | 106 | /** 107 | * Add a new login attempt. 108 | * 109 | * @return void 110 | */ 111 | public function addLoginAttempt() 112 | { 113 | $this->attempts++; 114 | $this->last_attempt_at = $this->freshTimeStamp(); 115 | 116 | if ($this->getLoginAttempts() >= static::$attemptLimit) 117 | { 118 | $this->suspend(); 119 | } 120 | else 121 | { 122 | $this->save(); 123 | } 124 | } 125 | 126 | /** 127 | * Clear all login attempts 128 | * 129 | * @return void 130 | */ 131 | public function clearLoginAttempts() 132 | { 133 | // If our login attempts is already at zero 134 | // we do not need to do anything. Additionally, 135 | // if we are suspended, we are not going to do 136 | // anything either as clearing login attempts 137 | // makes us unsuspended. We need to manually 138 | // call unsuspend() in order to unsuspend. 139 | if ($this->getLoginAttempts() == 0 or $this->suspended) 140 | { 141 | return; 142 | } 143 | 144 | $this->attempts = 0; 145 | $this->last_attempt_at = null; 146 | $this->suspended = false; 147 | $this->suspended_at = null; 148 | $this->save(); 149 | } 150 | 151 | /** 152 | * Suspend the user associated with the throttle 153 | * 154 | * @return void 155 | */ 156 | public function suspend() 157 | { 158 | if ( ! $this->suspended) 159 | { 160 | $this->suspended = true; 161 | $this->suspended_at = $this->freshTimeStamp(); 162 | $this->save(); 163 | } 164 | } 165 | 166 | /** 167 | * Unsuspend the user. 168 | * 169 | * @return void 170 | */ 171 | public function unsuspend() 172 | { 173 | if ($this->suspended) 174 | { 175 | $this->attempts = 0; 176 | $this->last_attempt_at = null; 177 | $this->suspended = false; 178 | $this->suspended_at = null; 179 | $this->save(); 180 | } 181 | } 182 | 183 | /** 184 | * Check if the user is suspended. 185 | * 186 | * @return bool 187 | */ 188 | public function isSuspended() 189 | { 190 | if ($this->suspended and $this->suspended_at) 191 | { 192 | $this->removeSuspensionIfAllowed(); 193 | return (bool) $this->suspended; 194 | } 195 | 196 | return false; 197 | } 198 | 199 | /** 200 | * Ban the user. 201 | * 202 | * @return void 203 | */ 204 | public function ban() 205 | { 206 | if ( ! $this->banned) 207 | { 208 | $this->banned = true; 209 | $this->banned_at = $this->freshTimeStamp(); 210 | $this->save(); 211 | } 212 | } 213 | 214 | /** 215 | * Unban the user. 216 | * 217 | * @return void 218 | */ 219 | public function unban() 220 | { 221 | if ($this->banned) 222 | { 223 | $this->banned = false; 224 | $this->banned_at = null; 225 | $this->save(); 226 | } 227 | } 228 | 229 | /** 230 | * Check if user is banned 231 | * 232 | * @return bool 233 | */ 234 | public function isBanned() 235 | { 236 | return $this->banned; 237 | } 238 | 239 | /** 240 | * Check user throttle status. 241 | * 242 | * @return bool 243 | * @throws \Cartalyst\Sentry\Throttling\UserBannedException 244 | * @throws \Cartalyst\Sentry\Throttling\UserSuspendedException 245 | */ 246 | public function check() 247 | { 248 | if ($this->isBanned()) 249 | { 250 | throw new UserBannedException(sprintf( 251 | 'User [%s] has been banned.', 252 | $this->getUser()->getLogin() 253 | )); 254 | } 255 | 256 | if ($this->isSuspended()) 257 | { 258 | throw new UserSuspendedException(sprintf( 259 | 'User [%s] has been suspended.', 260 | $this->getUser()->getLogin() 261 | )); 262 | } 263 | 264 | return true; 265 | } 266 | 267 | /** 268 | * User relationship for the throttle. 269 | * 270 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 271 | */ 272 | public function user() 273 | { 274 | return $this->belongsTo('Jenssegers\Mongodb\Sentry\User', 'user_id'); 275 | } 276 | 277 | /** 278 | * Inspects the last attempt vs the suspension time 279 | * (the time in which attempts must space before the 280 | * account is suspended). If we can clear our attempts 281 | * now, we'll do so and save. 282 | * 283 | * @return void 284 | */ 285 | public function clearLoginAttemptsIfAllowed() 286 | { 287 | $lastAttempt = clone $this->last_attempt_at; 288 | 289 | $suspensionTime = static::$suspensionTime; 290 | $clearAttemptsAt = $lastAttempt->modify("+{$suspensionTime} minutes"); 291 | $now = new DateTime; 292 | 293 | if ($clearAttemptsAt <= $now) 294 | { 295 | $this->attempts = 0; 296 | $this->save(); 297 | } 298 | 299 | unset($lastAttempt); 300 | unset($clearAttemptsAt); 301 | unset($now); 302 | } 303 | 304 | /** 305 | * Inspects to see if the user can become unsuspended 306 | * or not, based on the suspension time provided. If so, 307 | * unsuspends. 308 | * 309 | * @return void 310 | */ 311 | public function removeSuspensionIfAllowed() 312 | { 313 | $suspended = clone $this->suspended_at; 314 | 315 | $suspensionTime = static::$suspensionTime; 316 | $unsuspendAt = $suspended->modify("+{$suspensionTime} minutes"); 317 | $now = new DateTime; 318 | 319 | if ($unsuspendAt <= $now) 320 | { 321 | $this->unsuspend(); 322 | } 323 | 324 | unset($suspended); 325 | unset($unsuspendAt); 326 | unset($now); 327 | } 328 | 329 | /** 330 | * Get mutator for the suspended property. 331 | * 332 | * @param mixed $suspended 333 | * @return bool 334 | */ 335 | public function getSuspendedAttribute($suspended) 336 | { 337 | return (bool) $suspended; 338 | } 339 | 340 | /** 341 | * Get mutator for the banned property. 342 | * 343 | * @param mixed $banned 344 | * @return bool 345 | */ 346 | public function getBannedAttribute($banned) 347 | { 348 | return (bool) $banned; 349 | } 350 | 351 | /** 352 | * Get the attributes that should be converted to dates. 353 | * 354 | * @return array 355 | */ 356 | public function getDates() 357 | { 358 | return array_merge(parent::getDates(), array('last_attempt_at', 'suspended_at', 'banned_at')); 359 | } 360 | 361 | /** 362 | * Convert the model instance to an array. 363 | * 364 | * @return array 365 | */ 366 | public function toArray() 367 | { 368 | $result = parent::toArray(); 369 | 370 | if (isset($result['suspended'])) 371 | { 372 | $result['suspended'] = $this->getSuspendedAttribute($result['suspended']); 373 | } 374 | if (isset($result['banned'])) 375 | { 376 | $result['banned'] = $this->getBannedAttribute($result['banned']); 377 | } 378 | if (isset($result['last_attempt_at']) and $result['last_attempt_at'] instanceof DateTime) 379 | { 380 | $result['last_attempt_at'] = $result['last_attempt_at']->format('Y-m-d H:i:s'); 381 | } 382 | if (isset($result['suspended_at']) and $result['suspended_at'] instanceof DateTime) 383 | { 384 | $result['suspended_at'] = $result['suspended_at']->format('Y-m-d H:i:s'); 385 | } 386 | 387 | return $result; 388 | } 389 | 390 | /** 391 | * Set attempt limit. 392 | * 393 | * @param int $limit 394 | */ 395 | public static function setAttemptLimit($limit) 396 | { 397 | static::$attemptLimit = (int) $limit; 398 | } 399 | 400 | /** 401 | * Get attempt limit. 402 | * 403 | * @return int 404 | */ 405 | public static function getAttemptLimit() 406 | { 407 | return static::$attemptLimit; 408 | } 409 | 410 | /** 411 | * Set suspension time. 412 | * 413 | * @param int $minutes 414 | */ 415 | public static function setSuspensionTime($minutes) 416 | { 417 | static::$suspensionTime = (int) $minutes; 418 | } 419 | 420 | /** 421 | * Get suspension time. 422 | * 423 | * @return int 424 | */ 425 | public static function getSuspensionTime() 426 | { 427 | return static::$suspensionTime; 428 | } 429 | 430 | /** 431 | * Get the remaining time on a suspension in minutes rounded up. Returns 432 | * 0 if user is not suspended. 433 | * 434 | * @return int 435 | */ 436 | public function getRemainingSuspensionTime() 437 | { 438 | if(!$this->isSuspended()) 439 | return 0; 440 | 441 | $lastAttempt = clone $this->last_attempt_at; 442 | 443 | $suspensionTime = static::$suspensionTime; 444 | $clearAttemptsAt = $lastAttempt->modify("+{$suspensionTime} minutes"); 445 | $now = new Datetime; 446 | 447 | $timeLeft = $clearAttemptsAt->diff($now); 448 | 449 | $minutesLeft = ($timeLeft->s != 0 ? 450 | ($timeLeft->days * 24 * 60) + ($timeLeft->h * 60) + ($timeLeft->i) + 1 : 451 | ($timeLeft->days * 24 * 60) + ($timeLeft->h * 60) + ($timeLeft->i)); 452 | 453 | return $minutesLeft; 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/Jenssegers/Mongodb/Sentry/User.php: -------------------------------------------------------------------------------- 1 | Deny (adds to array, but denies regardless of user's group). 78 | * 0 => Remove. 79 | * 1 => Add. 80 | * 81 | * @var array 82 | */ 83 | protected $allowedPermissionsValues = array(-1, 0, 1); 84 | 85 | /** 86 | * The login attribute. 87 | * 88 | * @var string 89 | */ 90 | protected static $loginAttribute = 'email'; 91 | 92 | /** 93 | * The hasher the model uses. 94 | * 95 | * @var \Cartalyst\Sentry\Hashing\HasherInterface 96 | */ 97 | protected static $hasher; 98 | 99 | /** 100 | * The user groups. 101 | * 102 | * @var array 103 | */ 104 | protected $userGroups; 105 | 106 | /** 107 | * The user merged permissions. 108 | * 109 | * @var array 110 | */ 111 | protected $mergedPermissions; 112 | 113 | /** 114 | * The Eloquent group model. 115 | * 116 | * @var string 117 | */ 118 | protected static $groupModel = 'Jenssegers\Mongodb\Sentry\Group'; 119 | 120 | /** 121 | * The user groups pivot table name. 122 | * 123 | * @var string 124 | */ 125 | protected static $userGroupsPivot = 'users_groups'; 126 | 127 | /** 128 | * Returns the user's ID. 129 | * 130 | * @return mixed 131 | */ 132 | public function getId() 133 | { 134 | return $this->getKey(); 135 | } 136 | 137 | /** 138 | * Returns the name for the user's login. 139 | * 140 | * @return string 141 | */ 142 | public function getLoginName() 143 | { 144 | return static::$loginAttribute; 145 | } 146 | 147 | /** 148 | * Returns the user's login. 149 | * 150 | * @return mixed 151 | */ 152 | public function getLogin() 153 | { 154 | return $this->{$this->getLoginName()}; 155 | } 156 | 157 | /** 158 | * Returns the name for the user's password. 159 | * 160 | * @return string 161 | */ 162 | public function getPasswordName() 163 | { 164 | return 'password'; 165 | } 166 | 167 | /** 168 | * Returns the user's password (hashed). 169 | * 170 | * @return string 171 | */ 172 | public function getPassword() 173 | { 174 | return $this->password; 175 | } 176 | 177 | /** 178 | * Returns permissions for the user. 179 | * 180 | * @return array 181 | */ 182 | public function getPermissions() 183 | { 184 | return $this->permissions; 185 | } 186 | 187 | /** 188 | * Check if the user is activated. 189 | * 190 | * @return bool 191 | */ 192 | public function isActivated() 193 | { 194 | return (bool) $this->activated; 195 | } 196 | 197 | /** 198 | * Get mutator for giving the activated property. 199 | * 200 | * @param mixed $activated 201 | * @return bool 202 | */ 203 | public function getActivatedAttribute($activated) 204 | { 205 | return (bool) $activated; 206 | } 207 | 208 | /** 209 | * Mutator for giving permissions. 210 | * 211 | * @param mixed $permissions 212 | * @return array $_permissions 213 | */ 214 | public function getPermissionsAttribute($permissions) 215 | { 216 | if ( ! $permissions) 217 | { 218 | return array(); 219 | } 220 | 221 | if (is_array($permissions)) 222 | { 223 | return $permissions; 224 | } 225 | 226 | if ( ! $_permissions = json_decode($permissions, true)) 227 | { 228 | throw new \InvalidArgumentException("Cannot JSON decode permissions [$permissions]."); 229 | } 230 | 231 | return $_permissions; 232 | } 233 | 234 | /** 235 | * Mutator for taking permissions. 236 | * 237 | * @param array $permissions 238 | * @return string 239 | */ 240 | public function setPermissionsAttribute(array $permissions) 241 | { 242 | // Merge permissions 243 | $permissions = array_merge($this->getPermissions(), $permissions); 244 | 245 | // Loop through and adjust permissions as needed 246 | foreach ($permissions as $permission => &$value) 247 | { 248 | // Lets make sure there is a valid permission value 249 | if ( ! in_array($value = (int) $value, $this->allowedPermissionsValues)) 250 | { 251 | throw new \InvalidArgumentException("Invalid value [$value] for permission [$permission] given."); 252 | } 253 | 254 | // If the value is 0, delete it 255 | if ($value === 0) 256 | { 257 | unset($permissions[$permission]); 258 | } 259 | } 260 | 261 | $this->attributes['permissions'] = ( ! empty($permissions)) ? json_encode($permissions) : ''; 262 | } 263 | 264 | /** 265 | * Checks if the user is a super user - has 266 | * access to everything regardless of permissions. 267 | * 268 | * @return bool 269 | */ 270 | public function isSuperUser() 271 | { 272 | return $this->hasPermission('superuser'); 273 | } 274 | 275 | /** 276 | * Validates the user and throws a number of 277 | * Exceptions if validation fails. 278 | * 279 | * @return bool 280 | * @throws \Cartalyst\Sentry\Users\LoginRequiredException 281 | * @throws \Cartalyst\Sentry\Users\PasswordRequiredException 282 | * @throws \Cartalyst\Sentry\Users\UserExistsException 283 | */ 284 | public function validate() 285 | { 286 | if ( ! $login = $this->{static::$loginAttribute}) 287 | { 288 | throw new LoginRequiredException("A login is required for a user, none given."); 289 | } 290 | 291 | if ( ! $password = $this->getPassword()) 292 | { 293 | throw new PasswordRequiredException("A password is required for user [$login], none given."); 294 | } 295 | 296 | // Check if the user already exists 297 | $query = $this->newQuery(); 298 | $persistedUser = $query->where($this->getLoginName(), '=', $login)->first(); 299 | 300 | if ($persistedUser and $persistedUser->getId() != $this->getId()) 301 | { 302 | throw new UserExistsException("A user already exists with login [$login], logins must be unique for users."); 303 | } 304 | 305 | return true; 306 | } 307 | 308 | /** 309 | * Saves the user. 310 | * 311 | * @param array $options 312 | * @return bool 313 | */ 314 | public function save(array $options = array()) 315 | { 316 | $this->validate(); 317 | 318 | return parent::save($options); 319 | } 320 | 321 | /** 322 | * Delete the user. 323 | * 324 | * @return bool 325 | */ 326 | public function delete() 327 | { 328 | $this->groups()->detach(); 329 | return parent::delete(); 330 | } 331 | 332 | /** 333 | * Gets a code for when the user is 334 | * persisted to a cookie or session which 335 | * identifies the user. 336 | * 337 | * @return string 338 | */ 339 | public function getPersistCode() 340 | { 341 | $this->persist_code = $this->getRandomString(); 342 | 343 | // Our code got hashed 344 | $persistCode = $this->persist_code; 345 | 346 | $this->save(); 347 | 348 | return $persistCode; 349 | } 350 | 351 | /** 352 | * Checks the given persist code. 353 | * 354 | * @param string $persistCode 355 | * @return bool 356 | */ 357 | public function checkPersistCode($persistCode) 358 | { 359 | if ( ! $persistCode) 360 | { 361 | return false; 362 | } 363 | 364 | return $persistCode == $this->persist_code; 365 | } 366 | 367 | /** 368 | * Get an activation code for the given user. 369 | * 370 | * @return string 371 | */ 372 | public function getActivationCode() 373 | { 374 | $this->activation_code = $activationCode = $this->getRandomString(); 375 | 376 | $this->save(); 377 | 378 | return $activationCode; 379 | } 380 | 381 | /** 382 | * Attempts to activate the given user by checking 383 | * the activate code. If the user is activated already, 384 | * an Exception is thrown. 385 | * 386 | * @param string $activationCode 387 | * @return bool 388 | * @throws \Cartalyst\Sentry\Users\UserAlreadyActivatedException 389 | */ 390 | public function attemptActivation($activationCode) 391 | { 392 | if ($this->activated) 393 | { 394 | throw new UserAlreadyActivatedException('Cannot attempt activation on an already activated user.'); 395 | } 396 | 397 | if ($activationCode == $this->activation_code) 398 | { 399 | $this->activation_code = null; 400 | $this->activated = true; 401 | $this->activated_at = new DateTime; 402 | return $this->save(); 403 | } 404 | 405 | return false; 406 | } 407 | 408 | /** 409 | * Checks the password passed matches the user's password. 410 | * 411 | * @param string $password 412 | * @return bool 413 | */ 414 | public function checkPassword($password) 415 | { 416 | return $this->checkHash($password, $this->getPassword()); 417 | } 418 | 419 | /** 420 | * Get a reset password code for the given user. 421 | * 422 | * @return string 423 | */ 424 | public function getResetPasswordCode() 425 | { 426 | $this->reset_password_code = $resetCode = $this->getRandomString(); 427 | 428 | $this->save(); 429 | 430 | return $resetCode; 431 | } 432 | 433 | /** 434 | * Checks if the provided user reset password code is 435 | * valid without actually resetting the password. 436 | * 437 | * @param string $resetCode 438 | * @return bool 439 | */ 440 | public function checkResetPasswordCode($resetCode) 441 | { 442 | return ($this->reset_password_code == $resetCode); 443 | } 444 | 445 | /** 446 | * Attempts to reset a user's password by matching 447 | * the reset code generated with the user's. 448 | * 449 | * @param string $resetCode 450 | * @param string $newPassword 451 | * @return bool 452 | */ 453 | public function attemptResetPassword($resetCode, $newPassword) 454 | { 455 | if ($this->checkResetPasswordCode($resetCode)) 456 | { 457 | $this->password = $newPassword; 458 | $this->reset_password_code = null; 459 | return $this->save(); 460 | } 461 | 462 | return false; 463 | } 464 | 465 | /** 466 | * Wipes out the data associated with resetting 467 | * a password. 468 | * 469 | * @return void 470 | */ 471 | public function clearResetPassword() 472 | { 473 | if ($this->reset_password_code) 474 | { 475 | $this->reset_password_code = null; 476 | $this->save(); 477 | } 478 | } 479 | 480 | /** 481 | * Returns an array of groups which the given 482 | * user belongs to. 483 | * 484 | * @return array 485 | */ 486 | public function getGroups() 487 | { 488 | if ( ! $this->userGroups) 489 | { 490 | $this->userGroups = $this->groups()->get(); 491 | } 492 | 493 | return $this->userGroups; 494 | } 495 | 496 | /** 497 | * Adds the user to the given group. 498 | * 499 | * @param \Cartalyst\Sentry\Groups\GroupInterface $group 500 | * @return bool 501 | */ 502 | public function addGroup(GroupInterface $group) 503 | { 504 | if ( ! $this->inGroup($group)) 505 | { 506 | $this->groups()->attach($group); 507 | $this->userGroups = null; 508 | } 509 | 510 | return true; 511 | } 512 | 513 | /** 514 | * Removes the user from the given group. 515 | * 516 | * @param \Cartalyst\Sentry\Groups\GroupInterface $group 517 | * @return bool 518 | */ 519 | public function removeGroup(GroupInterface $group) 520 | { 521 | if ($this->inGroup($group)) 522 | { 523 | $this->groups()->detach($group); 524 | $this->userGroups = null; 525 | } 526 | 527 | return true; 528 | } 529 | 530 | /** 531 | * Updates the user to the given group(s). 532 | * 533 | * @param \Illuminate\Database\Eloquent\Collection $groups 534 | * @param bool $remove 535 | * @return bool 536 | */ 537 | public function updateGroups($groups, $remove = true) 538 | { 539 | $newGroupIds = array(); 540 | $removeGroupIds = array(); 541 | $existingGroupIds = array(); 542 | foreach ($groups as $group) 543 | { 544 | if (is_object($group)) 545 | { 546 | $newGroupIds[] = $group->getId(); 547 | if ( ! $this->addGroup($group)) 548 | { 549 | return false; 550 | } 551 | } 552 | else 553 | { 554 | $newGroupIds[] = $groups->getId(); 555 | if ( ! $this->addGroup($groups)) 556 | { 557 | return false; 558 | } 559 | break; 560 | } 561 | } 562 | if ($remove) 563 | { 564 | foreach ($this->groups as $userGroup) 565 | { 566 | $existingGroupIds[] = $userGroup->getId(); 567 | } 568 | $removeGroupIds = array_diff($existingGroupIds, $newGroupIds); 569 | if ($removeGroupIds) 570 | { 571 | self::$groupProviderModel = self::$groupProviderModel ?: new GroupProvider; 572 | } 573 | foreach ($removeGroupIds as $id) 574 | { 575 | $group = self::$groupProviderModel->findById($id); 576 | if ( ! $this->removeGroup($group)) 577 | { 578 | return false; 579 | } 580 | } 581 | } 582 | return true; 583 | } 584 | 585 | /** 586 | * See if the user is in the given group. 587 | * 588 | * @param \Cartalyst\Sentry\Groups\GroupInterface $group 589 | * @return bool 590 | */ 591 | public function inGroup(GroupInterface $group) 592 | { 593 | foreach ($this->getGroups() as $_group) 594 | { 595 | if ($_group->getId() == $group->getId()) 596 | { 597 | return true; 598 | } 599 | } 600 | 601 | return false; 602 | } 603 | 604 | /** 605 | * Returns an array of merged permissions for each 606 | * group the user is in. 607 | * 608 | * @return array 609 | */ 610 | public function getMergedPermissions() 611 | { 612 | if ( ! $this->mergedPermissions) 613 | { 614 | $permissions = array(); 615 | 616 | foreach ($this->getGroups() as $group) 617 | { 618 | $permissions = array_merge($permissions, $group->getPermissions()); 619 | } 620 | 621 | $this->mergedPermissions = array_merge($permissions, $this->getPermissions()); 622 | } 623 | 624 | return $this->mergedPermissions; 625 | } 626 | 627 | /** 628 | * See if a user has access to the passed permission(s). 629 | * Permissions are merged from all groups the user belongs to 630 | * and then are checked against the passed permission(s). 631 | * 632 | * If multiple permissions are passed, the user must 633 | * have access to all permissions passed through, unless the 634 | * "all" flag is set to false. 635 | * 636 | * Super users have access no matter what. 637 | * 638 | * @param string|array $permissions 639 | * @param bool $all 640 | * @return bool 641 | */ 642 | public function hasAccess($permissions, $all = true) 643 | { 644 | if ($this->isSuperUser()) 645 | { 646 | return true; 647 | } 648 | 649 | return $this->hasPermission($permissions, $all); 650 | } 651 | 652 | /** 653 | * See if a user has access to the passed permission(s). 654 | * Permissions are merged from all groups the user belongs to 655 | * and then are checked against the passed permission(s). 656 | * 657 | * If multiple permissions are passed, the user must 658 | * have access to all permissions passed through, unless the 659 | * "all" flag is set to false. 660 | * 661 | * Super users DON'T have access no matter what. 662 | * 663 | * @param string|array $permissions 664 | * @param bool $all 665 | * @return bool 666 | */ 667 | public function hasPermission($permissions, $all = true) 668 | { 669 | $mergedPermissions = $this->getMergedPermissions(); 670 | 671 | if ( ! is_array($permissions)) 672 | { 673 | $permissions = (array) $permissions; 674 | } 675 | 676 | foreach ($permissions as $permission) 677 | { 678 | // We will set a flag now for whether this permission was 679 | // matched at all. 680 | $matched = true; 681 | 682 | // Now, let's check if the permission ends in a wildcard "*" symbol. 683 | // If it does, we'll check through all the merged permissions to see 684 | // if a permission exists which matches the wildcard. 685 | if ((strlen($permission) > 1) and ends_with($permission, '*')) 686 | { 687 | $matched = false; 688 | 689 | foreach ($mergedPermissions as $mergedPermission => $value) 690 | { 691 | // Strip the '*' off the end of the permission. 692 | $checkPermission = substr($permission, 0, -1); 693 | 694 | // We will make sure that the merged permission does not 695 | // exactly match our permission, but starts with it. 696 | if ($checkPermission != $mergedPermission and starts_with($mergedPermission, $checkPermission) and $value == 1) 697 | { 698 | $matched = true; 699 | break; 700 | } 701 | } 702 | } 703 | 704 | elseif ((strlen($permission) > 1) and starts_with($permission, '*')) 705 | { 706 | $matched = false; 707 | 708 | foreach ($mergedPermissions as $mergedPermission => $value) 709 | { 710 | // Strip the '*' off the beginning of the permission. 711 | $checkPermission = substr($permission, 1); 712 | 713 | // We will make sure that the merged permission does not 714 | // exactly match our permission, but ends with it. 715 | if ($checkPermission != $mergedPermission and ends_with($mergedPermission, $checkPermission) and $value == 1) 716 | { 717 | $matched = true; 718 | break; 719 | } 720 | } 721 | } 722 | 723 | else 724 | { 725 | $matched = false; 726 | 727 | foreach ($mergedPermissions as $mergedPermission => $value) 728 | { 729 | // This time check if the mergedPermission ends in wildcard "*" symbol. 730 | if ((strlen($mergedPermission) > 1) and ends_with($mergedPermission, '*')) 731 | { 732 | $matched = false; 733 | 734 | // Strip the '*' off the end of the permission. 735 | $checkMergedPermission = substr($mergedPermission, 0, -1); 736 | 737 | // We will make sure that the merged permission does not 738 | // exactly match our permission, but starts with it. 739 | if ($checkMergedPermission != $permission and starts_with($permission, $checkMergedPermission) and $value == 1) 740 | { 741 | $matched = true; 742 | break; 743 | } 744 | } 745 | 746 | // Otherwise, we'll fallback to standard permissions checking where 747 | // we match that permissions explicitly exist. 748 | elseif ($permission == $mergedPermission and $mergedPermissions[$permission] == 1) 749 | { 750 | $matched = true; 751 | break; 752 | } 753 | } 754 | } 755 | 756 | // Now, we will check if we have to match all 757 | // permissions or any permission and return 758 | // accordingly. 759 | if ($all === true and $matched === false) 760 | { 761 | return false; 762 | } 763 | elseif ($all === false and $matched === true) 764 | { 765 | return true; 766 | } 767 | } 768 | 769 | if ($all === false) 770 | { 771 | return false; 772 | } 773 | 774 | return true; 775 | } 776 | 777 | /** 778 | * Returns if the user has access to any of the 779 | * given permissions. 780 | * 781 | * @param array $permissions 782 | * @return bool 783 | */ 784 | public function hasAnyAccess(array $permissions) 785 | { 786 | return $this->hasAccess($permissions, false); 787 | } 788 | 789 | /** 790 | * Records a login for the user. 791 | * 792 | * @return void 793 | */ 794 | public function recordLogin() 795 | { 796 | $this->last_login = new DateTime; 797 | $this->save(); 798 | } 799 | 800 | /** 801 | * Returns the relationship between users and groups. 802 | * 803 | * @return Illuminate\Database\Eloquent\Relations\BelongsToMany 804 | */ 805 | public function groups() 806 | { 807 | return $this->belongsToMany(static::$groupModel, static::$userGroupsPivot); 808 | } 809 | 810 | /** 811 | * Set the Eloquent model to use for group relationships. 812 | * 813 | * @param string $model 814 | * @return void 815 | */ 816 | public static function setGroupModel($model) 817 | { 818 | static::$groupModel = $model; 819 | } 820 | 821 | /** 822 | * Set the user groups pivot table name. 823 | * 824 | * @param string $tableName 825 | * @return void 826 | */ 827 | public static function setUserGroupsPivot($tableName) 828 | { 829 | static::$userGroupsPivot = $tableName; 830 | } 831 | 832 | /** 833 | * Check string against hashed string. 834 | * 835 | * @param string $string 836 | * @param string $hashedString 837 | * @return bool 838 | * @throws RuntimeException 839 | */ 840 | public function checkHash($string, $hashedString) 841 | { 842 | if ( ! static::$hasher) 843 | { 844 | throw new \RuntimeException("A hasher has not been provided for the user."); 845 | } 846 | 847 | return static::$hasher->checkHash($string, $hashedString); 848 | } 849 | 850 | /** 851 | * Hash string. 852 | * 853 | * @param string $string 854 | * @return string 855 | * @throws RuntimeException 856 | */ 857 | public function hash($string) 858 | { 859 | if ( ! static::$hasher) 860 | { 861 | throw new \RuntimeException("A hasher has not been provided for the user."); 862 | } 863 | 864 | return static::$hasher->hash($string); 865 | } 866 | 867 | /** 868 | * Generate a random string. 869 | * 870 | * @return string 871 | */ 872 | public function getRandomString($length = 42) 873 | { 874 | // We'll check if the user has OpenSSL installed with PHP. If they do 875 | // we'll use a better method of getting a random string. Otherwise, we'll 876 | // fallback to a reasonably reliable method. 877 | if (function_exists('openssl_random_pseudo_bytes')) 878 | { 879 | // We generate twice as many bytes here because we want to ensure we have 880 | // enough after we base64 encode it to get the length we need because we 881 | // take out the "/", "+", and "=" characters. 882 | $bytes = openssl_random_pseudo_bytes($length * 2); 883 | 884 | // We want to stop execution if the key fails because, well, that is bad. 885 | if ($bytes === false) 886 | { 887 | throw new \RuntimeException('Unable to generate random string.'); 888 | } 889 | 890 | return substr(str_replace(array('/', '+', '='), '', base64_encode($bytes)), 0, $length); 891 | } 892 | 893 | $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 894 | 895 | return substr(str_shuffle(str_repeat($pool, 5)), 0, $length); 896 | } 897 | 898 | /** 899 | * Returns an array of hashable attributes. 900 | * 901 | * @return array 902 | */ 903 | public function getHashableAttributes() 904 | { 905 | return $this->hashableAttributes; 906 | } 907 | 908 | /** 909 | * Set a given attribute on the model. 910 | * 911 | * @param string $key 912 | * @param mixed $value 913 | * @return void 914 | */ 915 | public function setAttribute($key, $value) 916 | { 917 | // Hash required fields when necessary 918 | if (in_array($key, $this->hashableAttributes) and ! empty($value)) 919 | { 920 | $value = $this->hash($value); 921 | } 922 | 923 | return parent::setAttribute($key, $value); 924 | } 925 | 926 | /** 927 | * Get the attributes that should be converted to dates. 928 | * 929 | * @return array 930 | */ 931 | public function getDates() 932 | { 933 | return array_merge(parent::getDates(), array('activated_at', 'last_login')); 934 | } 935 | 936 | /** 937 | * Convert the model instance to an array. 938 | * 939 | * @return array 940 | */ 941 | public function toArray() 942 | { 943 | $result = parent::toArray(); 944 | 945 | if (isset($result['activated'])) 946 | { 947 | $result['activated'] = $this->getActivatedAttribute($result['activated']); 948 | } 949 | if (isset($result['permissions'])) 950 | { 951 | $result['permissions'] = $this->getPermissionsAttribute($result['permissions']); 952 | } 953 | if (isset($result['suspended_at'])) 954 | { 955 | $result['suspended_at'] = $result['suspended_at']->format('Y-m-d H:i:s'); 956 | } 957 | 958 | return $result; 959 | } 960 | 961 | /** 962 | * Sets the hasher for the user. 963 | * 964 | * @param \Cartalyst\Sentry\Hashing\HasherInterface $hasher 965 | * @return void 966 | */ 967 | public static function setHasher(HasherInterface $hasher) 968 | { 969 | static::$hasher = $hasher; 970 | } 971 | 972 | /** 973 | * Returns the hasher for the user. 974 | * 975 | * @return \Cartalyst\Sentry\Hashing\HasherInterface 976 | */ 977 | public static function getHasher() 978 | { 979 | return static::$hasher; 980 | } 981 | 982 | /** 983 | * Unset the hasher used by the user. 984 | * 985 | * @return void 986 | */ 987 | public static function unsetHasher() 988 | { 989 | static::$hasher = null; 990 | } 991 | 992 | /** 993 | * Override the login attribute for all models instances. 994 | * 995 | * @param string $loginAttribute 996 | * @return void 997 | */ 998 | public static function setLoginAttributeName($loginAttribute) 999 | { 1000 | static::$loginAttribute = $loginAttribute; 1001 | } 1002 | 1003 | /** 1004 | * Get the current login attribute for all model instances. 1005 | * 1006 | * @return string 1007 | */ 1008 | public static function getLoginAttributeName() 1009 | { 1010 | return static::$loginAttribute; 1011 | } 1012 | 1013 | } 1014 | --------------------------------------------------------------------------------