Skip to content

Commit

Permalink
New flat auth tests. Inclues refactoring how user entity gets permiss…
Browse files Browse the repository at this point in the history
…ions. A couple of layers of caching on permissions. And a new ->can('fly') command on the entity.
  • Loading branch information
lonnieezell committed Nov 12, 2019
1 parent 58c46e8 commit 136098f
Show file tree
Hide file tree
Showing 8 changed files with 558 additions and 57 deletions.
42 changes: 5 additions & 37 deletions src/Authorization/FlatAuthorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,7 @@ public function addPermissionToUser($permission, int $userId)

if (! in_array($permissionId, $permissions))
{
$permissions[] = $permissionId;

$user->setPermissions($permissions);
$this->userModel->save($user);
$this->permissionModel->addPermissionToUser($permissionId, $user->id);
}

return true;
Expand All @@ -354,6 +351,8 @@ public function addPermissionToUser($permission, int $userId)
*
* @param int/string $permission
* @param int $userId
*
* @return bool|mixed|null
*/
public function removePermissionFromUser($permission, int $userId)
{
Expand All @@ -376,28 +375,7 @@ public function removePermissionFromUser($permission, int $userId)
return false;
}

$user = $this->userModel->find($userId);

if (! $user)
{
$this->error = lang('Auth.userNotFound', [$userId]);
return false;
}

// Grab the existing permissions for this user, and remove
// the permission id from the list.
$permissions = $user->getPermissions();

if (in_array($permissionId, $permissions))
{
unset($permissions[array_search($permissionId, $permissions)]);

$user->setPermissions($permissions);
$this->userModel->save($user);
}

// Save the updated permissions
return true;
return $this->permissionModel->removePermissionFromUser($permissionId, $userId);
}

/**
Expand All @@ -422,17 +400,7 @@ public function doesUserHavePermission($userId, $permission)
return null;
}

$user = $this->userModel->find($userId);

if (! $user)
{
$this->error = lang('Auth.userNotFound', [$userId]);
return false;
}

$permissions = $user->getPermissions();

return in_array($permissionId, $permissions);
return $this->permissionModel->doesUserHavePermission($userId, $permissionId);
}

//--------------------------------------------------------------------
Expand Down
88 changes: 82 additions & 6 deletions src/Authorization/PermissionModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@ class PermissionModel extends Model
*/
public function doesUserHavePermission(int $userId, int $permissionId): bool
{
// Check user permissions
$count = $this->db->table('auth_users_permissions')
->where('permission_id', $permissionId)
->where('user_id', $userId)
->countAll();
// Check user permissions and take advantage of caching
$userPerms = $this->getPermissionsForUser($userId);

if ($count > 0)
if (count($userPerms) && array_key_exists($permissionId, $userPerms))
{
return true;
}
Expand All @@ -48,4 +45,83 @@ public function doesUserHavePermission(int $userId, int $permissionId): bool

return $count > 0;
}

/**
* Adds a single permission to a single user.
*
* @param int $permissionId
* @param int $userId
*
* @return \CodeIgniter\Database\BaseResult|\CodeIgniter\Database\Query|false
*/
public function addPermissionToUser(int $permissionId, int $userId)
{
return $this->db->table('auth_users_permissions')->insert([
'user_id' => $userId,
'permission_id' => $permissionId
]);
}

/**
* Removes a permission from a user.
*
* @param int $permissionId
* @param int $userId
*
* @return mixed
*/
public function removePermissionFromUser(int $permissionId, int $userId)
{
$this->db->table('auth_users_permissions')->where([
'user_id' => $userId,
'permission_id' => $permissionId
])->delete();

cache()->delete("{$userId}_permissions");
}

/**
* Gets all permissions for a user in a way that can be
* easily used to check against:
*
* [
* id => name,
* id => name
* ]
*
* @param int $userId
*
* @return array
*/
public function getPermissionsForUser(int $userId): array
{
if (! $found = cache("{$userId}_permissions"))
{
$fromUser = $this->db->table('auth_users_permissions')
->select('id, auth_permissions.name')
->join('auth_permissions', 'auth_permissions.id = permission_id', 'inner')
->where('user_id', $userId)
->get()
->getResultObject();
$fromGroup = $this->db->table('auth_groups_users')
->select('auth_permissions.id, auth_permissions.name')
->join('auth_groups_permissions', 'auth_groups_permissions.group_id = auth_groups_users.group_id', 'inner')
->join('auth_permissions', 'auth_permissions.id = auth_groups_permissions.permission_id', 'inner')
->where('user_id', $userId)
->get()
->getResultObject();

$found = array_merge($fromUser, $fromGroup);

$result = [];
foreach ($found as $row)
{
$result[$row->id] = strtolower($row->name);
}

cache("{$userId}_permissions", $result, config('App'), 300);
}

return $result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public function up()
'status_message' => ['type' => 'varchar', 'constraint' => 255, 'null' => true],
'active' => ['type' => 'tinyint', 'constraint' => 1, 'null' => 0, 'default' => 0],
'force_pass_reset' => ['type' => 'tinyint', 'constraint' => 1, 'null' => 0, 'default' => 0],
'permissions' => ['type' => 'text', 'null' => true],
'created_at' => ['type' => 'datetime', 'null' => true],
'updated_at' => ['type' => 'datetime', 'null' => true],
'deleted_at' => ['type' => 'datetime', 'null' => true],
Expand Down
51 changes: 38 additions & 13 deletions src/Entities/User.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php namespace Myth\Auth\Entities;

use CodeIgniter\Entity;
use Myth\Auth\Authorization\PermissionModel;

class User extends Entity
{
Expand Down Expand Up @@ -30,6 +31,12 @@ class User extends Entity
'force_pass_reset' => 'boolean',
];

/**
* Per-user permissions cache
* @var array
*/
protected $permissions = [];

/**
* Automatically hashes the password when set.
*
Expand Down Expand Up @@ -183,33 +190,51 @@ public function isBanned(): bool
}

/**
* Returns the user's permissions, automatically
* json_decoding them into an associative array.
* Determines whether the user has the appropriate permission,
* either directly, or through one of it's groups.
*
* @param string $permission
*
* @return bool
*/
public function can(string $permission)
{
$permission = strtolower($permission);
$permissions = $this->getPermissions();

return in_array($permission, $permissions);
}

/**
* Returns the user's permissions, formatted for simple checking:
*
* [
* id => name,
* id=> name,
* ]
*
* @return array|mixed
*/
public function getPermissions()
{
return ! empty($this->attributes['permissions'])
? json_decode($this->attributes['permissions'], true)
: [];
if (empty($this->permissions))
{
$this->permissions = (new PermissionModel())->getPermissionsForUser($this->id);
}

return $this->permissions;
}

/**
* Stores the permissions, automatically json_encoding
* them for saving.
* Warns the developer it won't work, so they don't spend
* hours tracking stuff down.
*
* @param array $permissions
*
* @return $this
*/
public function setPermissions(array $permissions = null)
{
if (is_array($permissions))
{
$this->attributes['permissions'] = json_encode($permissions);
}

return $this;
throw new \RuntimeException('User entity does not support saving permissions directly.');
}
}
65 changes: 65 additions & 0 deletions tests/UserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

use Myth\Auth\Entities\User;
use ModuleTests\Support\AuthTestCase;

class UserTest extends AuthTestCase
{
public function testGetPermissionsThroughUser()
{
$user = $this->createUser();
$permission1 = $this->createPermission(['name' => 'first']);
$permission2 = $this->createPermission(['name' => 'second']);

$this->hasInDatabase('auth_users_permissions', [
'user_id' => $user->id,
'permission_id' => $permission1->id
]);
$this->hasInDatabase('auth_users_permissions', [
'user_id' => $user->id,
'permission_id' => $permission2->id
]);

$expected = [
$permission1->id => $permission1->name,
$permission2->id => $permission2->name,
];

$this->assertEquals($expected, $user->permissions);
}

public function testGetPermissionsThroughGroup()
{
$user = $this->createUser();
$group = $this->createGroup();
$permission = $this->createPermission(['name' => 'first']);

$this->hasInDatabase('auth_groups_permissions', [
'group_id' => $group->id,
'permission_id' => $permission->id
]);
$this->hasInDatabase('auth_groups_users', [
'user_id' => $user->id,
'group_id' => $group->id
]);

$expected = [
$permission->id => $permission->name,
];

$this->assertEquals($expected, $user->permissions);
}

public function testCan()
{
$user = $this->createUser();
$permission = $this->createPermission();
$this->hasInDatabase('auth_users_permissions', [
'user_id' => $user->id,
'permission_id' => $permission->id
]);

$this->assertTrue($user->can($permission->name));
$this->assertFalse($user->can('jump for joy'));
}
}
3 changes: 3 additions & 0 deletions tests/_support/AuthTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ protected function createUser(array $info = [])
$userId = $this->users->insert($user);
$user = $this->users->find($userId);

// Delete any cached permissions
cache()->delete("{$userId}_permissions");

return $user;
}

Expand Down
Loading

0 comments on commit 136098f

Please sign in to comment.