Access.php 4.55 KB
Newer Older
1
2
3
4
<?php

namespace Source\UseCases\Door\Access;

Jacob Priddy's avatar
Jacob Priddy committed
5
use Carbon\Carbon;
6
use Source\Entities\User;
7
8
9
use Source\Entities\Entry;
use Source\Entities\Attempt;
use Source\Sanitize\CastsTo;
10
11
use Source\Entities\HashedSearchable;
use Source\Gateways\Users\UsersRepository;
12
13
use Source\Exceptions\AuthorizationException;
use Source\Exceptions\AuthenticationException;
14
15
use Source\Gateways\Entries\EntriesRepository;
use Source\Gateways\Attempts\AttemptsRepository;
16
17
18

class Access implements AccessUseCase
{
19
    public const COMMAND_DELIMITER = '*';
20
21
22
23
24
25
26
27
28
29
30
31
    use CastsTo;

    /**
     * @var \Source\Gateways\Attempts\AttemptsRepository
     */
    protected AttemptsRepository $attempts;

    /**
     * @var \Source\Gateways\Entries\EntriesRepository
     */
    protected EntriesRepository $entries;

32
    /**
33
     * @var AccessAuthorizer[]
34
     */
35
36
37
38
39
40
41
42
    protected array $authorizers = [];

    /**
     * @var \Source\Gateways\Users\UsersRepository
     */
    protected UsersRepository $users;

    protected string $salt;
43

44
    public function __construct(
45
        UsersRepository $users,
46
        AttemptsRepository $attempts,
47
48
49
        EntriesRepository $entries,
        string $salt,
        AccessAuthorizer ...$authorizers
50
    ) {
51
        $this->users = $users;
52
        $this->attempts = $attempts;
Jacob Priddy's avatar
Jacob Priddy committed
53
        $this->entries = $entries;
54
55
        $this->salt = $salt;

Jacob Priddy's avatar
Jacob Priddy committed
56
        $this->authorizers = $authorizers;
Jacob Priddy's avatar
Jacob Priddy committed
57
58
    }

59
60
61
62
63
    /**
     * @param string $doorId
     * @param string $userId
     * @param bool   $success
     */
Jacob Priddy's avatar
Jacob Priddy committed
64
65
66
67
68
69
70
71
72
73
    protected function logUserAccess(string $doorId, string $userId, bool $success): void
    {
        $this->entries->add(new Entry(
            0,
            $userId,
            $doorId,
            $success
        ));
    }

74
75
76
    /**
     * @param string $doorId
     */
Jacob Priddy's avatar
Jacob Priddy committed
77
78
    protected function logDoorAttempt(string $doorId): void
    {
79
        $this->attempts->add(new Attempt(0, self::castToInt($doorId)));
80
81
    }

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
    /**
     * @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);
        }
    }

106
107
108
    /**
     * @inheritDoc
     */
109
    public function protectUserDoorAccessAtTime(?string $doorId, string $doorcode, Carbon $date): void
110
    {
Jacob Priddy's avatar
Jacob Priddy committed
111
112
        /*
         * 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
113
         * as well as process any sent commands
Jacob Priddy's avatar
Jacob Priddy committed
114
115
116
117
118
119
120
121
122
123
124
         *
         * 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
         * during the current time frame based on their assigned group schedules.
         *
         * door ---> group <--- user
         *              ^
         *              |
         *              |
         *          schedule
         *
125
126
         * 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
Jacob Priddy's avatar
Jacob Priddy committed
127
         */
128

129
        if ($doorId === null) {
130
131
132
            throw new AuthenticationException();
        }

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
        $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;
            }
153
        }
154

155
156
        // Default to deny if no authorizer allowed us
        $this->logDeny($user, $doorId);
Jacob Priddy's avatar
Jacob Priddy committed
157
        throw new AuthorizationException();
158
159
    }
}