Commit 893d5f46 authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

Rework door access authorizer and start on door commands

parent 75e0714e
......@@ -29,5 +29,7 @@ class GroupsSeeder extends Seeder
$groups->create(LocalGroupsRepository::getManageGroupsGroup());
$groups->create(LocalGroupsRepository::getLogViewGroup());
$groups->create(LocalGroupsRepository::getDoorCommanderGroup());
}
}
......@@ -12,4 +12,5 @@ class Permissions
public const CODE_QUERY = 'code-query';
public const CURRENT_USER = 'current-user';
public const LOGS_READ = 'logs-read';
public const DOOR_COMMANDER = 'door-commander';
}
......@@ -25,6 +25,8 @@ class LocalGroupsRepository extends InMemoryGroupsRepository
$this->create(static::getManageGroupsGroup());
$this->create(static::getLogViewGroup());
$this->create(static::getDoorCommanderGroup());
}
/**
......@@ -116,4 +118,13 @@ class LocalGroupsRepository extends InMemoryGroupsRepository
'Gives permission to view logs.'
);
}
public static function getDoorCommanderGroup(): Group
{
return new Group(
11,
Permissions::DOOR_COMMANDER,
'Gives permission to enter commands at doors and to enter a door at any time'
);
}
}
......@@ -3,43 +3,22 @@
namespace Source\UseCases\Door\Access;
use Carbon\Carbon;
use Source\Entities\User;
use Source\Entities\Entry;
use Source\Entities\Attempt;
use Source\Sanitize\CastsTo;
use Source\Entities\HashedSearchable;
use Source\Gateways\Users\UsersRepository;
use Source\Exceptions\AuthorizationException;
use Source\Exceptions\AuthenticationException;
use Source\Gateways\Entries\EntriesRepository;
use Source\Gateways\Attempts\AttemptsRepository;
use Source\UseCases\DoorUserGroup\DoorUserGroupMapUseCase;
use Source\UseCases\Door\Access\Authorizers\DoorOpenModeCheck\DoorOpenModeCheck;
use Source\UseCases\Door\Access\Authorizers\DoorOverrideCheck\DoorOverrideCheck;
use Source\UseCases\Door\Access\Authorizers\UserDoorcodeCheck\UserDoorcodeCheck;
use Source\UseCases\Door\Access\Authorizers\GroupScheduleCheck\GroupScheduleCheck;
class Access implements AccessUseCase
{
public const COMMAND_DELIMITER = '*';
use CastsTo;
/**
* @var \Source\UseCases\Door\Access\Authorizers\DoorOpenModeCheck\DoorOpenModeCheck
*/
protected DoorOpenModeCheck $openModeCheck;
/**
* @var \Source\UseCases\Door\Access\Authorizers\UserDoorcodeCheck\UserDoorcodeCheck
*/
protected UserDoorcodeCheck $userCheck;
/**
* @var \Source\UseCases\Door\Access\Authorizers\GroupScheduleCheck\GroupScheduleCheck
*/
protected GroupScheduleCheck $scheduleCheck;
/**
* @var \Source\UseCases\DoorUserGroup\DoorUserGroupMapUseCase
*/
protected DoorUserGroupMapUseCase $doorUserMapper;
/**
* @var \Source\Gateways\Attempts\AttemptsRepository
*/
......@@ -51,28 +30,39 @@ class Access implements AccessUseCase
protected EntriesRepository $entries;
/**
* @var \Source\UseCases\Door\Access\Authorizers\DoorOverrideCheck\DoorOverrideCheck
* @var AccessAuthorizer[]
*/
protected DoorOverrideCheck $overrideCheck;
protected array $authorizers = [];
/**
* @var \Source\Gateways\Users\UsersRepository
*/
protected UsersRepository $users;
protected string $salt;
public function __construct(
DoorOpenModeCheck $openModeCheck,
UserDoorcodeCheck $userCheck,
GroupScheduleCheck $scheduleCheck,
DoorUserGroupMapUseCase $doorUserMapper,
DoorOverrideCheck $overrideCheck,
UsersRepository $users,
AttemptsRepository $attempts,
EntriesRepository $entries
EntriesRepository $entries,
string $salt,
AccessAuthorizer ...$authorizers
) {
$this->openModeCheck = $openModeCheck;
$this->userCheck = $userCheck;
$this->scheduleCheck = $scheduleCheck;
$this->doorUserMapper = $doorUserMapper;
$this->users = $users;
$this->attempts = $attempts;
$this->entries = $entries;
$this->overrideCheck = $overrideCheck;
$this->salt = $salt;
foreach ($authorizers as $authorizer) {
$this->authorizers[] = $authorizer;
}
}
/**
* @param string $doorId
* @param string $userId
* @param bool $success
*/
protected function logUserAccess(string $doorId, string $userId, bool $success): void
{
$this->entries->add(new Entry(
......@@ -83,18 +73,46 @@ class Access implements AccessUseCase
));
}
/**
* @param string $doorId
*/
protected function logDoorAttempt(string $doorId): void
{
$this->attempts->add(new Attempt(0, $this->castToInt($doorId)));
}
/**
* @param \Source\Entities\User|null $user
* @param string $doorId
*/
protected function logAllow(?User $user, string $doorId): void
{
if ($user) {
$this->logUserAccess($doorId, $user->getId(), true);
}
}
/**
* @param \Source\Entities\User|null $user
* @param string $doorId
*/
protected function logDeny(?User $user, string $doorId): void
{
if ($user) {
$this->logUserAccess($doorId, $user->getId(), false);
} else {
$this->logDoorAttempt($doorId);
}
}
/**
* @inheritDoc
*/
public function protectUserDoorAccess(?string $doorId, ?string $doorcode): void
public function protectUserDoorAccessAtTime(?string $doorId, string $doorcode, Carbon $date): void
{
/*
* Our job here is to find out if a user has access given a doorcode and the id of the door they want to access
* as well as process any sent commands
*
* This process is a little complicated as we not only have to check whether the user has access to the door
* but also if the door might possibly be allowing everyone (open mode), or if the user has access to the door
......@@ -106,89 +124,38 @@ class Access implements AccessUseCase
* |
* schedule
*
* Checks (action):
* 1. Check doorId
* - door (continue)
* - no door (deny)
* 2. Check for door overrides
* - door in open mode (allow)
* - door in closed mode (deny)
* - no override (continue)
* 3. Check for open mode on the door
* - open mode on (allow)
* - open mode off (continue)
* 4. Get user from door code
* - user exists (continue)
* - no user found (deny and log attempt)
* 5. Get group intersection of doors and users
* 6. Foreach group check all user access schedules
* - schedule has currently active event (allow and log entry)
* - schedule doe snot have currently active event (continue)
* 7. Now all allowing conditions hit (deny and log entry)
* I have broken this process into small chunks that can easily be enabled and disabled, or changed
* These authorizers are injected in the service provider, and we will go until allowed or denied
*/
if (!$doorId) {
throw new AuthenticationException();
}
$now = Carbon::now();
/*
* Check overrides
*/
$overrideStatus = $this->overrideCheck->checkForOverrides($doorId, $now);
if ($overrideStatus === DoorOverrideCheck::OPEN) {
return;
}
if ($overrideStatus === DoorOverrideCheck::CLOSED) {
throw new AuthorizationException();
}
/*
* Check for open mode
*/
if ($this->openModeCheck->checkOpenMode($doorId, $now)) {
// No logging needed to be done as we are in open mode. Just return without throwing any exceptions
return;
}
/*
* Check for user
*/
if (!($user = $this->userCheck->checkDoorCode($doorcode))) {
$this->logDoorAttempt($doorId);
throw new AuthorizationException();
}
$userId = $user->getId();
$groups = $this->doorUserMapper->getGroupsForDoorUserIntersection($doorId, $userId);
/*
* Could also check if any groups exist first if wanted here.
* Wanted if not doing schedule checking
* Don't currently need this as no groups in the schedule checking will not allow as there are no groups
* to check schedule for
*/
// if (!$groups) {
// // Log that the user does not have access, but tried to access the door
// $this->logUserAccess($doorId, $userId, false);
// throw new AuthorizationException();
// }
/*
* Check for user schedule
*/
if ($this->scheduleCheck->checkScheduleForGroups($groups, $now)) {
// Log the successful entry
$this->logUserAccess($doorId, $userId, true);
return;
$parts = explode(self::COMMAND_DELIMITER, $doorcode);
$code = $parts[0] ?? '';
$command = $parts[1] ?? null;
$user = $this->users->findByDoorcode(HashedSearchable::hash($this->salt, $code));
foreach ($this->authorizers as $authorizer) {
switch ($authorizer->check($user, $date, $doorId, $code, $command)) {
case AccessAuthorizer::ALLOW:
$this->logAllow($user, $doorId);
return;
case AccessAuthorizer::DENY:
$this->logDeny($user, $doorId);
throw new AuthorizationException();
case AccessAuthorizer::CONTINUE:
// Fall through to default.
default:
break;
}
}
// Log that the user does not have access, but tried to access the door
$this->logUserAccess($doorId, $userId, false);
// Default to deny if no authorizer allowed us
$this->logDeny($user, $doorId);
throw new AuthorizationException();
}
}
<?php
namespace Source\UseCases\Door\Access;
use Carbon\Carbon;
use Source\Entities\User;
interface AccessAuthorizer
{
public const ALLOW = 0;
public const DENY = 1;
public const CONTINUE = 2;
/**
* Must return an access authorizer status
*
* @param \Source\Entities\User|null $user
* @param \Carbon\Carbon $date
* @param string $doorId
* @param string $doorcode
* @param string $commandString
* @return int
*/
public function check(?User $user, Carbon $date, string $doorId, string $doorcode, ?string $commandString): int;
}
......@@ -3,13 +3,16 @@
namespace Source\UseCases\Door\Access;
use Carbon\Carbon;
interface AccessUseCase
{
/**
* @param string|null $doorId
* @param string|null $doorcode
* @param string|null $doorId
* @param string|null $doorcode
* @param \Carbon\Carbon $date
* @throws \Source\Exceptions\AuthenticationException
* @throws \Source\Exceptions\AuthorizationException
*/
public function protectUserDoorAccess(?string $doorId, ?string $doorcode): void;
public function protectUserDoorAccessAtTime(?string $doorId, string $doorcode, Carbon $date): void;
}
......@@ -8,20 +8,12 @@ use Source\Gateways\Users\UsersRepository;
use Source\Gateways\Entries\EntriesRepository;
use Illuminate\Contracts\Foundation\Application;
use Source\Gateways\Attempts\AttemptsRepository;
use Source\Gateways\Overrides\OverridesRepository;
use Source\Gateways\Schedules\SchedulesRepository;
use Illuminate\Contracts\Support\DeferrableProvider;
use Source\Gateways\DoorSchedule\DoorScheduleRepository;
use Source\Gateways\RecurrenceSet\RecurrenceSetRepository;
use Source\UseCases\DoorUserGroup\DoorUserGroupMapUseCase;
use Source\UseCases\Door\Access\Authorizers\DoorOpenModeCheck\DoorOpenModeCheck;
use Source\UseCases\Door\Access\Authorizers\DoorOverrideCheck\DoorOverrideCheck;
use Source\UseCases\Door\Access\Authorizers\UserDoorcodeCheck\UserDoorcodeCheck;
use Source\UseCases\Door\Access\Authorizers\GroupScheduleCheck\GroupScheduleCheck;
use Source\UseCases\Door\Access\Authorizers\DoorOpenModeCheck\DefaultDoorOpenModeCheck;
use Source\UseCases\Door\Access\Authorizers\DoorOverrideCheck\DefaultDoorOverrideCheck;
use Source\UseCases\Door\Access\Authorizers\UserDoorcodeCheck\DefaultUserDoorcodeCheck;
use Source\UseCases\Door\Access\Authorizers\GroupScheduleCheck\DefaultGroupScheduleCheck;
use Source\UseCases\Door\Access\Authorizers\CommandAuthorizer;
use Source\UseCases\Door\Access\Authorizers\OverrideAuthorizer;
use Source\UseCases\Door\Access\Authorizers\OpenModeAuthorizer;
use Source\UseCases\Door\Access\Authorizers\ScheduleAuthorizer;
use Source\UseCases\Door\Access\Authorizers\CommanderAuthorizer;
/**
* Service provider must be registered in AppServiceProvider
......@@ -35,40 +27,26 @@ class AccessUseCaseServiceProvider extends ServiceProvider implements Deferrable
*/
public function register()
{
$this->app->bind(DoorOpenModeCheck::class, static function (Application $app) {
return new DefaultDoorOpenModeCheck(
$app->make(DoorScheduleRepository::class),
$app->make(RecurrenceSetRepository::class)
);
});
$this->app->bind(GroupScheduleCheck::class, static function (Application $app) {
return new DefaultGroupScheduleCheck(
$app->make(SchedulesRepository::class),
$app->make(RecurrenceSetRepository::class)
);
});
$this->app->bind(UserDoorcodeCheck::class, static function (Application $app) {
return new DefaultUserDoorcodeCheck(
config('app.key'),
$app->make(UsersRepository::class)
);
});
$this->app->bind(DoorOverrideCheck::class, static function (Application $app) {
return new DefaultDoorOverrideCheck($app->make(OverridesRepository::class));
});
$this->app->bind(AccessUseCase::class, static function (Application $app) {
return new Access(
$app->make(DoorOpenModeCheck::class),
$app->make(UserDoorcodeCheck::class),
$app->make(GroupScheduleCheck::class),
$app->make(DoorUserGroupMapUseCase::class),
$app->make(DoorOverrideCheck::class),
$app->make(UsersRepository::class),
$app->make(AttemptsRepository::class),
$app->make(EntriesRepository::class)
$app->make(EntriesRepository::class),
config('app.key'),
/*
* Order matters here because override will deny for high level groups as it assumes groups are
* checked first. Also command can deny for somebody allowed even when door is in open mode to deny cmd:
* Command check
* High level group Check
* Override check
* Open mode schedule check
* User schedule check
*/
$app->make(CommandAuthorizer::class),
$app->make(CommanderAuthorizer::class),
$app->make(OverrideAuthorizer::class),
$app->make(OpenModeAuthorizer::class),
$app->make(ScheduleAuthorizer::class)
);
});
}
......@@ -87,12 +65,6 @@ class AccessUseCaseServiceProvider extends ServiceProvider implements Deferrable
*/
public function provides()
{
return [
AccessUseCase::class,
DoorOpenModeCheck::class,
GroupScheduleCheck::class,
UserDoorcodeCheck::class,
DoorOverrideCheck::class,
];
return [AccessUseCase::class];
}
}
<?php
namespace Source\UseCases\Door\Access\Authorizers;
use Carbon\Carbon;
use Source\Entities\User;
use Source\Authorization\Permissions;
use Source\Authorization\ApiAuthorizer;
use Source\UseCases\Door\Access\AccessAuthorizer;
class CommandAuthorizer implements AccessAuthorizer
{
/**
* @var \Source\Authorization\ApiAuthorizer
*/
protected ApiAuthorizer $authorizer;
public function __construct(ApiAuthorizer $authorizer)
{
$this->authorizer = $authorizer;
}
/**
* @inheritDoc
*/
public function check(?User $user, Carbon $date, string $doorId, string $doorcode, ?string $commandString): int
{
if ($commandString) {
if ($this->authorizer->allowsOne([Permissions::DOOR_COMMANDER, Permissions::MANAGE_DOORS])) {
}
return self::DENY;
}
return self::CONTINUE;
}
}
<?php
namespace Source\UseCases\Door\Access\Authorizers;
use Carbon\Carbon;
use Source\Entities\User;
use Source\Authorization\Permissions;
use Source\Authorization\ApiAuthorizer;
use Source\Exceptions\EntityNotFoundException;
use Source\UseCases\Door\Access\AccessAuthorizer;
class CommanderAuthorizer implements AccessAuthorizer
{
/**
* @var \Source\Authorization\ApiAuthorizer
*/
protected ApiAuthorizer $authorizer;
public function __construct(ApiAuthorizer $authorizer)
{
$this->authorizer = $authorizer;
}
/**
* @inheritDoc
*/
public function check(?User $user, Carbon $date, string $doorId, string $doorcode, ?string $commandString): int
{
if ($user) {
$this->authorizer->setCurrentUserId($user->getId());
try {
if ($this->authorizer->allowsOne([Permissions::DOOR_COMMANDER, Permissions::MANAGE_DOORS])) {
return self::ALLOW;
}
} catch (EntityNotFoundException $e) {
// Keep calm and continue on
}
}
return self::CONTINUE;
}
}
<?php
namespace Source\UseCases\Door\Access\Authorizers\DoorOpenModeCheck;
use Carbon\Carbon;
interface DoorOpenModeCheck
{
/**
* Checks for the door to be in open mode at a specified date.
* Returns false if it is not in open mode
* Returns true if the door is in open mode and should be allowed
*
* @param string $doorId
* @param \Carbon\Carbon $date
* @return bool
*/
public function checkOpenMode(string $doorId, Carbon $date): bool;
}
<?php
namespace Source\UseCases\Door\Access\Authorizers\DoorOverrideCheck;
use Carbon\Carbon;
interface DoorOverrideCheck
{
public const OPEN = 0;
public const CLOSED = 1;
public const NO_ACTION = 2;
/**
* Returns one of the constants defined on DoorOverrideCheck
*
* @param string $doorId
* @param \Carbon\Carbon $date
* @return mixed
*/
public function checkForOverrides(string $doorId, Carbon $date): int;
}
<?php
namespace Source\UseCases\Door\Access\Authorizers\GroupScheduleCheck;
use Carbon\Carbon;
use InvalidArgumentException;
use Source\Entities\Schedule;
use Source\Gateways\Schedules\SchedulesRepository;
use Source\Gateways\RecurrenceSet\RecurrenceSetRepository;
class DefaultGroupScheduleCheck implements GroupScheduleCheck