Browse Source

Merge remote-tracking branch 'remotes/origin/user-rewrite'

J. King 1 month ago
parent
commit
63ae6fb703

+ 2
- 1
README.md View File

@@ -93,7 +93,8 @@ As a general rule, The Arsse should yield the same output as the reference imple
93 93
 - When marking articles as starred the feed ID is ignored, as they are not needed to establish uniqueness
94 94
 - The feed updater ignores the `userId` parameter: feeds in The Arsse are deduplicated, and have no owner
95 95
 - The `/feeds/all` route lists only feeds which should be checked for updates, and it also returns all `userId` attributes as empty strings: feeds in The Arsse are deduplicated, and have no owner
96
-- The updater console commands mentioned in the protocol specification are not implemented, as The Arsse does not implement the required NextCloud subsystems
96
+- The API's "updater" routes do not require administrator priviledges as The Arsse has no concept of user classes
97
+- The "updater" console commands mentioned in the protocol specification are not implemented, as The Arsse does not implement the required NextCloud subsystems
97 98
 - The `lastLoginTimestamp` attribute of the user metadata is always the current time: The Arsse's implementation of the protocol is fully stateless
98 99
 
99 100
 #### Ambiguities

+ 0
- 2
lib/CLI.php View File

@@ -47,8 +47,6 @@ USAGE_TEXT;
47 47
     protected function loadConf(): bool {
48 48
         $conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
49 49
         Arsse::load($conf);
50
-        // command-line operations will never respect authorization
51
-        Arsse::$user->authorizationEnabled(false);
52 50
         return true;
53 51
     }
54 52
 

+ 12
- 85
lib/Database.php View File

@@ -6,7 +6,6 @@
6 6
 declare(strict_types=1);
7 7
 namespace JKingWeb\Arsse;
8 8
 
9
-use PasswordGenerator\Generator as PassGen;
10 9
 use JKingWeb\DrUUID\UUID;
11 10
 use JKingWeb\Arsse\Misc\Query;
12 11
 use JKingWeb\Arsse\Misc\Context;
@@ -83,7 +82,7 @@ class Database {
83 82
         return $out;
84 83
     }
85 84
 
86
-    protected function generateIn(array $values, string $type) {
85
+    protected function generateIn(array $values, string $type): array {
87 86
         $out = [
88 87
             [], // query clause
89 88
             [], // binding types
@@ -122,21 +121,15 @@ class Database {
122 121
         return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id = ?", "str")->run($user)->getValue();
123 122
     }
124 123
 
125
-    public function userAdd(string $user, string $password = null): string {
124
+    public function userAdd(string $user, string $password): bool {
126 125
         if (!Arsse::$user->authorize($user, __FUNCTION__)) {
127 126
             throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
128 127
         } elseif ($this->userExists($user)) {
129 128
             throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
130 129
         }
131
-        if ($password===null) {
132
-            $password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
133
-        }
134
-        $hash = "";
135
-        if (strlen($password) > 0) {
136
-            $hash = password_hash($password, \PASSWORD_DEFAULT);
137
-        }
130
+        $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : "";
138 131
         $this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
139
-        return $password;
132
+        return true;
140 133
     }
141 134
 
142 135
     public function userRemove(string $user): bool {
@@ -149,24 +142,13 @@ class Database {
149 142
         return true;
150 143
     }
151 144
 
152
-    public function userList(string $domain = null): array {
145
+    public function userList(): array {
153 146
         $out = [];
154
-        if ($domain !== null) {
155
-            if (!Arsse::$user->authorize("@".$domain, __FUNCTION__)) {
156
-                throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
157
-            }
158
-            $domain = str_replace(["\\","%","_"], ["\\\\", "\\%", "\\_"], $domain);
159
-            $domain = "%@".$domain;
160
-            foreach ($this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain) as $user) {
161
-                $out[] = $user['id'];
162
-            }
163
-        } else {
164
-            if (!Arsse::$user->authorize("", __FUNCTION__)) {
165
-                throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
166
-            }
167
-            foreach ($this->db->query("SELECT id from arsse_users") as $user) {
168
-                $out[] = $user['id'];
169
-            }
147
+        if (!Arsse::$user->authorize("", __FUNCTION__)) {
148
+            throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => ""]);
149
+        }
150
+        foreach ($this->db->query("SELECT id from arsse_users") as $user) {
151
+            $out[] = $user['id'];
170 152
         }
171 153
         return $out;
172 154
     }
@@ -180,66 +162,14 @@ class Database {
180 162
         return (string) $this->db->prepare("SELECT password from arsse_users where id = ?", "str")->run($user)->getValue();
181 163
     }
182 164
 
183
-    public function userPasswordSet(string $user, string $password = null): string {
165
+    public function userPasswordSet(string $user, string $password): bool {
184 166
         if (!Arsse::$user->authorize($user, __FUNCTION__)) {
185 167
             throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
186 168
         } elseif (!$this->userExists($user)) {
187 169
             throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
188 170
         }
189
-        if ($password===null) {
190
-            $password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
191
-        }
192
-        $hash = "";
193
-        if (strlen($password) > 0) {
194
-            $hash = password_hash($password, \PASSWORD_DEFAULT);
195
-        }
171
+        $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : "";
196 172
         $this->db->prepare("UPDATE arsse_users set password = ? where id = ?", "str", "str")->run($hash, $user);
197
-        return $password;
198
-    }
199
-
200
-    public function userPropertiesGet(string $user): array {
201
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
202
-            throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
203
-        }
204
-        $prop = $this->db->prepare("SELECT name,rights from arsse_users where id = ?", "str")->run($user)->getRow();
205
-        if (!$prop) {
206
-            throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
207
-        }
208
-        return $prop;
209
-    }
210
-
211
-    public function userPropertiesSet(string $user, array $properties): array {
212
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
213
-            throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
214
-        } elseif (!$this->userExists($user)) {
215
-            throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
216
-        }
217
-        $valid = [ // FIXME: add future properties
218
-            "name" => "str",
219
-        ];
220
-        list($setClause, $setTypes, $setValues) = $this->generateSet($properties, $valid);
221
-        if (!$setClause) {
222
-            // if no changes would actually be applied, just return
223
-            return $this->userPropertiesGet($user);
224
-        }
225
-        $this->db->prepare("UPDATE arsse_users set $setClause where id = ?", $setTypes, "str")->run($setValues, $user);
226
-        return $this->userPropertiesGet($user);
227
-    }
228
-
229
-    public function userRightsGet(string $user): int {
230
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
231
-            throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
232
-        }
233
-        return (int) $this->db->prepare("SELECT rights from arsse_users where id = ?", "str")->run($user)->getValue();
234
-    }
235
-
236
-    public function userRightsSet(string $user, int $rights): bool {
237
-        if (!Arsse::$user->authorize($user, __FUNCTION__, $rights)) {
238
-            throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
239
-        } elseif (!$this->userExists($user)) {
240
-            throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
241
-        }
242
-        $this->db->prepare("UPDATE arsse_users set rights = ? where id = ?", "int", "str")->run($rights, $user);
243 173
         return true;
244 174
     }
245 175
 
@@ -596,10 +526,7 @@ class Database {
596 526
         if (!ValueInfo::id($id)) {
597 527
             throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]);
598 528
         }
599
-        // disable authorization checks for the list call
600
-        Arsse::$user->authorizationEnabled(false);
601 529
         $sub = $this->subscriptionList($user, null, true, (int) $id)->getRow();
602
-        Arsse::$user->authorizationEnabled(true);
603 530
         if (!$sub) {
604 531
             throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
605 532
         }

+ 2
- 2
lib/Db/AbstractResult.php View File

@@ -36,9 +36,9 @@ abstract class AbstractResult implements Result {
36 36
         return iterator_to_array($this, false);
37 37
     }
38 38
 
39
-    abstract public function changes();
39
+    abstract public function changes(): int;
40 40
 
41
-    abstract public function lastId();
41
+    abstract public function lastId(): int;
42 42
 
43 43
     // PHP iterator methods
44 44
 

+ 1
- 1
lib/REST.php View File

@@ -135,7 +135,7 @@ class REST {
135 135
             $user = $env['REMOTE_USER'];
136 136
         }
137 137
         if (strlen($user)) {
138
-            if (Arsse::$user->auth($user, $password)) {
138
+            if (Arsse::$user->auth((string) $user, (string) $password)) {
139 139
                 $req = $req->withAttribute("authenticated", true);
140 140
                 $req = $req->withAttribute("authenticatedUser", $user);
141 141
             } else {

+ 4
- 32
lib/REST/NextCloudNews/V1_2.php View File

@@ -365,10 +365,6 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
365 365
 
366 366
     // return list of feeds which should be refreshed
367 367
     protected function feedListStale(array $url, array $data): ResponseInterface {
368
-        // function requires admin rights per spec
369
-        if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
370
-            return new EmptyResponse(403);
371
-        }
372 368
         // list stale feeds which should be checked for updates
373 369
         $feeds = Arsse::$db->feedListStale();
374 370
         $out = [];
@@ -381,10 +377,6 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
381 377
 
382 378
     // refresh a feed
383 379
     protected function feedUpdate(array $url, array $data): ResponseInterface {
384
-        // function requires admin rights per spec
385
-        if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
386
-            return new EmptyResponse(403);
387
-        }
388 380
         try {
389 381
             Arsse::$db->feedUpdate($data['feedId']);
390 382
         } catch (ExceptionInput $e) {
@@ -659,40 +651,20 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
659 651
     }
660 652
 
661 653
     protected function userStatus(array $url, array $data): ResponseInterface {
662
-        $data = Arsse::$user->propertiesGet(Arsse::$user->id, true);
663
-        // construct the avatar structure, if an image is available
664
-        if (isset($data['avatar'])) {
665
-            $avatar = [
666
-                'data' => base64_encode($data['avatar']['data']),
667
-                'mime' => (string) $data['avatar']['type'],
668
-            ];
669
-        } else {
670
-            $avatar = null;
671
-        }
672
-        // construct the rest of the structure
673
-        $out = [
654
+        return new Response([
674 655
             'userId' => (string) Arsse::$user->id,
675
-            'displayName' => (string) ($data['name'] ?? Arsse::$user->id),
656
+            'displayName' => (string) Arsse::$user->id,
676 657
             'lastLoginTimestamp' => time(),
677
-            'avatar' => $avatar,
678
-        ];
679
-        return new Response($out);
658
+            'avatar' => null,
659
+        ]);
680 660
     }
681 661
 
682 662
     protected function cleanupBefore(array $url, array $data): ResponseInterface {
683
-        // function requires admin rights per spec
684
-        if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
685
-            return new EmptyResponse(403);
686
-        }
687 663
         Service::cleanupPre();
688 664
         return new EmptyResponse(204);
689 665
     }
690 666
 
691 667
     protected function cleanupAfter(array $url, array $data): ResponseInterface {
692
-        // function requires admin rights per spec
693
-        if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
694
-            return new EmptyResponse(403);
695
-        }
696 668
         Service::cleanupPost();
697 669
         return new EmptyResponse(204);
698 670
     }

+ 48
- 391
lib/User.php View File

@@ -6,12 +6,9 @@
6 6
 declare(strict_types=1);
7 7
 namespace JKingWeb\Arsse;
8 8
 
9
+use PasswordGenerator\Generator as PassGen;
10
+
9 11
 class User {
10
-    const RIGHTS_NONE           = 0;    // normal user
11
-    const RIGHTS_DOMAIN_MANAGER = 25;   // able to act for any normal users on same domain; cannot elevate other users
12
-    const RIGHTS_DOMAIN_ADMIN   = 50;   // able to act for any users on same domain not above themselves; may elevate users on same domain to domain manager or domain admin
13
-    const RIGHTS_GLOBAL_MANAGER = 75;   // able to act for any normal users on any domain; cannot elevate other users
14
-    const RIGHTS_GLOBAL_ADMIN   = 100;  // is completely unrestricted
15 12
 
16 13
     public $id = null;
17 14
 
@@ -19,9 +16,6 @@ class User {
19 16
     * @var User\Driver
20 17
     */
21 18
     protected $u;
22
-    protected $authz = 0;
23
-    protected $authzSupported = 0;
24
-    protected $actor = [];
25 19
 
26 20
     public static function driverList(): array {
27 21
         $sep = \DIRECTORY_SEPARATOR;
@@ -35,426 +29,89 @@ class User {
35 29
         return $classes;
36 30
     }
37 31
 
38
-    public function __construct() {
39
-        $driver = Arsse::$conf->userDriver;
40
-        $this->u = new $driver();
32
+    public function __construct(\JKingWeb\Arsse\User\Driver $driver = null) {
33
+        $this->u = $driver ?? new Arsse::$conf->userDriver;
41 34
     }
42 35
 
43 36
     public function __toString() {
44
-        if ($this->id===null) {
45
-            $this->credentials();
46
-        }
47 37
         return (string) $this->id;
48 38
     }
49 39
 
50
-    // checks whether the logged in user is authorized to act for the affected user (used especially when granting rights)
51
-    public function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool {
52
-        // if authorization checks are disabled (either because we're running the installer or the background updater) just return true
53
-        if (!$this->authorizationEnabled()) {
54
-            return true;
55
-        }
56
-        // if we don't have a logged-in user, fetch credentials
57
-        if ($this->id===null) {
58
-            $this->credentials();
59
-        }
60
-        // if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
61
-        if ($affectedUser==Arsse::$user->id && $action != "userRightsSet") {
62
-            return true;
63
-        }
64
-        // if we're authorizing something other than a user function and the affected user is not the actor, make sure the affected user exists
65
-        $this->authorizationEnabled(false);
66
-        if (Arsse::$user->id != $affectedUser && strpos($action, "user")!==0 && !$this->exists($affectedUser)) {
67
-            throw new User\Exception("doesNotExist", ["action" => $action, "user" => $affectedUser]);
68
-        }
69
-        $this->authorizationEnabled(true);
70
-        // get properties of actor if not already available
71
-        if (!sizeof($this->actor)) {
72
-            $this->actor = $this->propertiesGet(Arsse::$user->id);
73
-        }
74
-        $rights = $this->actor["rights"];
75
-        // if actor is a global admin, accept the request
76
-        if ($rights==User\Driver::RIGHTS_GLOBAL_ADMIN) {
77
-            return true;
78
-        }
79
-        // if actor is a common user, deny the request
80
-        if ($rights==User\Driver::RIGHTS_NONE) {
81
-            return false;
82
-        }
83
-        // if actor is not some other sort of admin, deny the request
84
-        if (!in_array($rights, [User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN], true)) {
85
-            return false;
86
-        }
87
-        // if actor is a domain admin/manager and domains don't match, deny the request
88
-        if ($this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
89
-            $test = "@".$this->actor["domain"];
90
-            if (substr($affectedUser, -1*strlen($test)) != $test) {
91
-                return false;
92
-            }
93
-        }
94
-        // certain actions shouldn't check affected user's rights
95
-        if (in_array($action, ["userRightsGet","userExists","userList"], true)) {
96
-            return true;
97
-        }
98
-        if ($action=="userRightsSet") {
99
-            // setting rights above your own is not allowed
100
-            if ($newRightsLevel > $rights) {
101
-                return false;
102
-            }
103
-            // setting yourself to rights you already have is harmless and can be allowed
104
-            if ($this->id==$affectedUser && $newRightsLevel==$rights) {
105
-                return true;
106
-            }
107
-            // managers can only set their own rights, and only to normal user
108
-            if (in_array($rights, [User\Driver::RIGHTS_DOMAIN_MANAGER, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
109
-                if ($this->id != $affectedUser || $newRightsLevel != User\Driver::RIGHTS_NONE) {
110
-                    return false;
111
-                }
112
-                return true;
113
-            }
114
-        }
115
-        $affectedRights = $this->rightsGet($affectedUser);
116
-        // managers can only act on themselves (checked above) or regular users
117
-        if (in_array($rights, [User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER]) && $affectedRights != User\Driver::RIGHTS_NONE) {
118
-            return false;
119
-        }
120
-        // domain admins canot act above themselves
121
-        if (!in_array($affectedRights, [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN])) {
122
-            return false;
123
-        }
124
-        return true;
125
-    }
126
-
127
-    public function credentials(): array {
128
-        if ($_SERVER['PHP_AUTH_USER']) {
129
-            $out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']];
130
-        } elseif ($_SERVER['REMOTE_USER']) {
131
-            $out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""];
132
-        } else {
133
-            $out = ["user" => "", "password" => ""];
134
-        }
135
-        $this->id = $out["user"];
136
-        return $out;
40
+    public function authorize(string $affectedUser, string $action): bool {
41
+        // at one time there was a complicated authorization system; it exists vestigially to support a later revival if desired
42
+        return $this->u->authorize($affectedUser, $action);
137 43
     }
138 44
 
139
-    public function auth(string $user = null, string $password = null): bool {
140
-        if ($user===null) {
141
-            return $this->authHTTP();
45
+    public function auth(string $user, string $password): bool {
46
+        $prevUser = $this->id;
47
+        $this->id = $user;
48
+        if (Arsse::$conf->userPreAuth) {
49
+            $out = true;
142 50
         } else {
143
-            $prevUser = $this->id ?? null;
144
-            $this->id = $user;
145
-            $this->actor = [];
146
-            switch ($this->u->driverFunctions("auth")) {
147
-                case User\Driver::FUNC_EXTERNAL:
148
-                    if (Arsse::$conf->userPreAuth) {
149
-                        $out = true;
150
-                    } else {
151
-                        $out = $this->u->auth($user, $password);
152
-                    }
153
-                    if ($out && !Arsse::$db->userExists($user)) {
154
-                        $this->autoProvision($user, $password);
155
-                    }
156
-                    break;
157
-                case User\Driver::FUNC_INTERNAL:
158
-                    if (Arsse::$conf->userPreAuth) {
159
-                        if (!Arsse::$db->userExists($user)) {
160
-                            $this->autoProvision($user, $password);
161
-                        }
162
-                        $out = true;
163
-                    } else {
164
-                        $out = $this->u->auth($user, $password);
165
-                    }
166
-                    break;
167
-                case User\Driver::FUNCT_NOT_IMPLEMENTED:
168
-                    $out = false;
169
-                    break;
170
-            }
171
-            if (!$out) {
172
-                $this->id = $prevUser;
173
-            }
174
-            return $out;
51
+            $out = $this->u->auth($user, $password);
175 52
         }
176
-    }
177
-
178
-    public function authHTTP(): bool {
179
-        $cred = $this->credentials();
180
-        if (!$cred["user"]) {
181
-            return false;
53
+        // if authentication was successful and we don't have the user in the internal database, add it
54
+        // users must be in the internal database to preserve referential integrity
55
+        if ($out && !Arsse::$db->userExists($user)) {
56
+            Arsse::$db->userAdd($user, $password);
182 57
         }
183
-        return $this->auth($cred["user"], $cred["password"]);
184
-    }
185
-
186
-    public function driverFunctions(string $function = null) {
187
-        return $this->u->driverFunctions($function);
58
+        $this->id = $prevUser;
59
+        return $out;
188 60
     }
189 61
 
190
-    public function list(string $domain = null): array {
62
+    public function list(): array {
191 63
         $func = "userList";
192
-        switch ($this->u->driverFunctions($func)) {
193
-            case User\Driver::FUNC_EXTERNAL:
194
-                // we handle authorization checks for external drivers
195
-                if ($domain===null) {
196
-                    if (!$this->authorize("@".$domain, $func)) {
197
-                        throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $domain]);
198
-                    }
199
-                } else {
200
-                    if (!$this->authorize("", $func)) {
201
-                        throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => "all users"]);
202
-                    }
203
-                }
204
-                // no break
205
-            case User\Driver::FUNC_INTERNAL:
206
-                // internal functions handle their own authorization
207
-                return $this->u->userList($domain);
208
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
209
-                throw new User\ExceptionNotImplemented("notImplemented", ["action" => $func, "user" => $domain]);
210
-        }
211
-    }
212
-
213
-    public function authorizationEnabled(bool $setting = null): bool {
214
-        if (is_null($setting)) {
215
-            return !$this->authz;
216
-        }
217
-        $this->authz += ($setting ? -1 : 1);
218
-        if ($this->authz < 0) {
219
-            $this->authz = 0;
64
+        if (!$this->authorize("", $func)) {
65
+            throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => ""]);
220 66
         }
221
-        return !$this->authz;
67
+        return $this->u->userList();
222 68
     }
223 69
 
224 70
     public function exists(string $user): bool {
225 71
         $func = "userExists";
226
-        switch ($this->u->driverFunctions($func)) {
227
-            case User\Driver::FUNC_EXTERNAL:
228
-                // we handle authorization checks for external drivers
229
-                if (!$this->authorize($user, $func)) {
230
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
231
-                }
232
-                $out = $this->u->userExists($user);
233
-                if ($out && !Arsse::$db->userExists($user)) {
234
-                    $this->autoProvision($user, "");
235
-                }
236
-                return $out;
237
-            case User\Driver::FUNC_INTERNAL:
238
-                // internal functions handle their own authorization
239
-                return $this->u->userExists($user);
240
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
241
-                // throwing an exception here would break all kinds of stuff; we just report that the user exists
242
-                return true;
72
+        if (!$this->authorize($user, $func)) {
73
+            throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
243 74
         }
75
+        return $this->u->userExists($user);
244 76
     }
245 77
 
246 78
     public function add($user, $password = null): string {
247 79
         $func = "userAdd";
248
-        switch ($this->u->driverFunctions($func)) {
249
-            case User\Driver::FUNC_EXTERNAL:
250
-                // we handle authorization checks for external drivers
251
-                if (!$this->authorize($user, $func)) {
252
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
253
-                }
254
-                $newPassword = $this->u->userAdd($user, $password);
255
-                // if there was no exception and we don't have the user in the internal database, add it
256
-                if (!Arsse::$db->userExists($user)) {
257
-                    $this->autoProvision($user, $newPassword);
258
-                }
259
-                return $newPassword;
260
-            case User\Driver::FUNC_INTERNAL:
261
-                // internal functions handle their own authorization
262
-                return $this->u->userAdd($user, $password);
263
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
264
-                throw new User\ExceptionNotImplemented("notImplemented", ["action" => $func, "user" => $user]);
80
+        if (!$this->authorize($user, $func)) {
81
+            throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
265 82
         }
83
+        return $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword());
266 84
     }
267 85
 
268 86
     public function remove(string $user): bool {
269 87
         $func = "userRemove";
270
-        switch ($this->u->driverFunctions($func)) {
271
-            case User\Driver::FUNC_EXTERNAL:
272
-                // we handle authorization checks for external drivers
273
-                if (!$this->authorize($user, $func)) {
274
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
275
-                }
276
-                $out = $this->u->userRemove($user);
277
-                if ($out && Arsse::$db->userExists($user)) {
278
-                    // if the user was removed and we have it in our data, remove it there
279
-                    if (!Arsse::$db->userExists($user)) {
280
-                        Arsse::$db->userRemove($user);
281
-                    }
282
-                }
283
-                return $out;
284
-            case User\Driver::FUNC_INTERNAL:
285
-                // internal functions handle their own authorization
286
-                return $this->u->userRemove($user);
287
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
288
-                throw new User\ExceptionNotImplemented("notImplemented", ["action" => $func, "user" => $user]);
88
+        if (!$this->authorize($user, $func)) {
89
+            throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
90
+        }
91
+        try {
92
+            return $this->u->userRemove($user);
93
+        } finally { // @codeCoverageIgnore
94
+            if (Arsse::$db->userExists($user)) {
95
+                // if the user was removed and we (still) have it in the internal database, remove it there
96
+                Arsse::$db->userRemove($user);
97
+            }
289 98
         }
290 99
     }
291 100
 
292 101
     public function passwordSet(string $user, string $newPassword = null, $oldPassword = null): string {
293 102
         $func = "userPasswordSet";
294
-        switch ($this->u->driverFunctions($func)) {
295
-            case User\Driver::FUNC_EXTERNAL:
296
-                // we handle authorization checks for external drivers
297
-                if (!$this->authorize($user, $func)) {
298
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
299
-                }
300
-                $out = $this->u->userPasswordSet($user, $newPassword, $oldPassword);
301
-                if (Arsse::$db->userExists($user)) {
302
-                    // if the password change was successful and the user exists, set the internal password to the same value
303
-                    Arsse::$db->userPasswordSet($user, $out);
304
-                } else {
305
-                    // if the user does not exists in the internal database, create it
306
-                    $this->autoProvision($user, $out);
307
-                }
308
-                return $out;
309
-            case User\Driver::FUNC_INTERNAL:
310
-                // internal functions handle their own authorization
311
-                return $this->u->userPasswordSet($user, $newPassword);
312
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
313
-                throw new User\ExceptionNotImplemented("notImplemented", ["action" => $func, "user" => $user]);
314
-        }
315
-    }
316
-
317
-    public function propertiesGet(string $user, bool $withAvatar = false): array {
318
-        // prepare default values
319
-        $domain = null;
320
-        if (strrpos($user, "@")!==false) {
321
-            $domain = substr($user, strrpos($user, "@")+1);
103
+        if (!$this->authorize($user, $func)) {
104
+            throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
322 105
         }
323
-        $init = [
324
-            "id"     => $user,
325
-            "name"   => $user,
326
-            "rights" => User\Driver::RIGHTS_NONE,
327
-            "domain" => $domain
328
-        ];
329
-        $func = "userPropertiesGet";
330
-        switch ($this->u->driverFunctions($func)) {
331
-            case User\Driver::FUNC_EXTERNAL:
332
-                // we handle authorization checks for external drivers
333
-                if (!$this->authorize($user, $func)) {
334
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
335
-                }
336
-                $out = array_merge($init, $this->u->userPropertiesGet($user));
337
-                // remove password if it is return (not exhaustive, but...)
338
-                if (array_key_exists('password', $out)) {
339
-                    unset($out['password']);
340
-                }
341
-                // if the user does not exist in the internal database, add it
342
-                if (!Arsse::$db->userExists($user)) {
343
-                    $this->autoProvision($user, "", $out);
344
-                }
345
-                return $out;
346
-            case User\Driver::FUNC_INTERNAL:
347
-                // internal functions handle their own authorization
348
-                return array_merge($init, $this->u->userPropertiesGet($user));
349
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
350
-                // we can return generic values if the function is not implemented
351
-                return $init;
352
-        }
353
-    }
354
-
355
-    public function propertiesSet(string $user, array $properties): array {
356
-        // remove from the array any values which should be set specially
357
-        foreach (['id', 'domain', 'password', 'rights'] as $key) {
358
-            if (array_key_exists($key, $properties)) {
359
-                unset($properties[$key]);
360
-            }
361
-        }
362
-        $func = "userPropertiesSet";
363
-        switch ($this->u->driverFunctions($func)) {
364
-            case User\Driver::FUNC_EXTERNAL:
365
-                // we handle authorization checks for external drivers
366
-                if (!$this->authorize($user, $func)) {
367
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
368
-                }
369
-                $out = $this->u->userPropertiesSet($user, $properties);
370
-                if (Arsse::$db->userExists($user)) {
371
-                    // if the property change was successful and the user exists, set the internal properties to the same values
372
-                    Arsse::$db->userPropertiesSet($user, $out);
373
-                } else {
374
-                    // if the user does not exists in the internal database, create it
375
-                    $this->autoProvision($user, "", $out);
376
-                }
377
-                return $out;
378
-            case User\Driver::FUNC_INTERNAL:
379
-                // internal functions handle their own authorization
380
-                return $this->u->userPropertiesSet($user, $properties);
381
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
382
-                throw new User\ExceptionNotImplemented("notImplemented", ["action" => $func, "user" => $user]);
383
-        }
384
-    }
385
-
386
-    public function rightsGet(string $user): int {
387
-        $func = "userRightsGet";
388
-        switch ($this->u->driverFunctions($func)) {
389
-            case User\Driver::FUNC_EXTERNAL:
390
-                // we handle authorization checks for external drivers
391
-                if (!$this->authorize($user, $func)) {
392
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
393
-                }
394
-                $out = $this->u->userRightsGet($user);
395
-                // if the user does not exist in the internal database, add it
396
-                if (!Arsse::$db->userExists($user)) {
397
-                    $this->autoProvision($user, "", null, $out);
398
-                }
399
-                return $out;
400
-            case User\Driver::FUNC_INTERNAL:
401
-                // internal functions handle their own authorization
402
-                return $this->u->userRightsGet($user);
403
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
404
-                // assume all users are unprivileged
405
-                return User\Driver::RIGHTS_NONE;
406
-        }
407
-    }
408
-
409
-    public function rightsSet(string $user, int $level): bool {
410
-        $func = "userRightsSet";
411
-        switch ($this->u->driverFunctions($func)) {
412
-            case User\Driver::FUNC_EXTERNAL:
413
-                // we handle authorization checks for external drivers
414
-                if (!$this->authorize($user, $func)) {
415
-                    throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
416
-                }
417
-                $out = $this->u->userRightsSet($user, $level);
418
-                // if the user does not exist in the internal database, add it
419
-                if ($out && Arsse::$db->userExists($user)) {
420
-                    $authz = $this->authorizationEnabled();
421
-                    $this->authorizationEnabled(false);
422
-                    Arsse::$db->userRightsSet($user, $level);
423
-                    $this->authorizationEnabled($authz);
424
-                } elseif ($out) {
425
-                    $this->autoProvision($user, "", null, $level);
426
-                }
427
-                return $out;
428
-            case User\Driver::FUNC_INTERNAL:
429
-                // internal functions handle their own authorization
430
-                return $this->u->userRightsSet($user, $level);
431
-            case User\Driver::FUNCT_NOT_IMPLEMENTED:
432
-                throw new User\ExceptionNotImplemented("notImplemented", ["action" => $func, "user" => $user]);
106
+        $out = $this->u->userPasswordSet($user, $newPassword, $oldPassword) ?? $this->u->userPasswordSet($user, $this->generatePassword(), $oldPassword);
107
+        if (Arsse::$db->userExists($user)) {
108
+            // if the password change was successful and the user exists, set the internal password to the same value
109
+            Arsse::$db->userPasswordSet($user, $out);
433 110
         }
111
+        return $out;
434 112
     }
435 113
 
436
-    protected function autoProvision(string $user, string $password = null, array $properties = null, int $rights = 0): string {
437
-        // temporarily disable authorization checks, to avoid potential problems
438
-        $this->authorizationEnabled(false);
439
-        // create the user
440
-        $out = Arsse::$db->userAdd($user, $password);
441
-        // set the user rights
442
-        Arsse::$db->userRightsSet($user, $rights);
443
-        // set the user properties...
444
-        if ($properties===null) {
445
-            // if nothing is provided but the driver uses an external function, try to get the current values from the external source
446
-            try {
447
-                if ($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) {
448
-                    Arsse::$db->userPropertiesSet($user, $this->u->userPropertiesGet($user));
449
-                }
450
-            } catch (\Throwable $e) {
451
-            }
452
-        } else {
453
-            // otherwise if values are provided, use those
454
-            Arsse::$db->userPropertiesSet($user, $properties);
455
-        }
456
-        // re-enable authorization and return
457
-        $this->authorizationEnabled(true);
458
-        return $out;
114
+    protected function generatePassword(): string {
115
+        return (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
459 116
     }
460 117
 }

+ 5
- 19
lib/User/Driver.php View File

@@ -11,36 +11,22 @@ interface Driver {
11 11
     const FUNC_INTERNAL = 1;
12 12
     const FUNC_EXTERNAL = 2;
13 13
 
14
-    const RIGHTS_NONE           = 0;    // normal user
15
-    const RIGHTS_DOMAIN_MANAGER = 25;   // able to act for any normal users on same domain; cannot elevate other users
16
-    const RIGHTS_DOMAIN_ADMIN   = 50;   // able to act for any users on same domain not above themselves; may elevate users on same domain to domain manager or domain admin
17
-    const RIGHTS_GLOBAL_MANAGER = 75;   // able to act for any normal users on any domain; cannot elevate other users
18
-    const RIGHTS_GLOBAL_ADMIN   = 100;  // is completely unrestricted
19
-
20 14
     // returns an instance of a class implementing this interface.
21 15
     public function __construct();
22 16
     // returns a human-friendly name for the driver (for display in installer, for example)
23 17
     public static function driverName(): string;
24
-    // returns an array (or single queried member of same) of methods defined by this interface and whether the class implements the internal function or a custom version
25
-    public function driverFunctions(string $function = null);
26 18
     // authenticates a user against their name and password
27 19
     public function auth(string $user, string $password): bool;
20
+    // check whether a user is authorized to perform a certain action; not currently used and subject to change
21
+    public function authorize(string $affectedUser, string $action): bool;
28 22
     // checks whether a user exists
29 23
     public function userExists(string $user): bool;
30 24
     // adds a user
31
-    public function userAdd(string $user, string $password = null): string;
25
+    public function userAdd(string $user, string $password = null);
32 26
     // removes a user
33 27
     public function userRemove(string $user): bool;
34 28
     // lists all users
35
-    public function userList(string $domain = null): array;
29
+    public function userList(): array;
36 30
     // sets a user's password; if the driver does not require the old password, it may be ignored
37
-    public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string;
38
-    // gets user metadata (currently not useful)
39
-    public function userPropertiesGet(string $user): array;
40
-    // sets user metadata (currently not useful)
41
-    public function userPropertiesSet(string $user, array $properties): array;
42
-    // returns a user's access level according to RIGHTS_* constants (or some custom semantics, if using custom implementation of authorize())
43
-    public function userRightsGet(string $user): int;
44
-    // sets a user's access level
45
-    public function userRightsSet(string $user, int $level): bool;
31
+    public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null);
46 32
 }

+ 46
- 24
lib/User/Internal/Driver.php View File

@@ -6,37 +6,59 @@
6 6
 declare(strict_types=1);
7 7
 namespace JKingWeb\Arsse\User\Internal;
8 8
 
9
-final class Driver implements \JKingWeb\Arsse\User\Driver {
10
-    use InternalFunctions;
11
-
12
-    protected $db;
13
-    protected $functions = [
14
-        "auth"                    => self::FUNC_INTERNAL,
15
-        "userList"                => self::FUNC_INTERNAL,
16
-        "userExists"              => self::FUNC_INTERNAL,
17
-        "userAdd"                 => self::FUNC_INTERNAL,
18
-        "userRemove"              => self::FUNC_INTERNAL,
19
-        "userPasswordSet"         => self::FUNC_INTERNAL,
20
-        "userPropertiesGet"       => self::FUNC_INTERNAL,
21
-        "userPropertiesSet"       => self::FUNC_INTERNAL,
22
-        "userRightsGet"           => self::FUNC_INTERNAL,
23
-        "userRightsSet"           => self::FUNC_INTERNAL,
24
-    ];
9
+use JKingWeb\Arsse\Arsse;
10
+use JKingWeb\Arsse\User\Exception;
11
+
12
+class Driver implements \JKingWeb\Arsse\User\Driver {
13
+    public function __construct() {
14
+    }
25 15
 
26 16
     public static function driverName(): string {
27 17
         return Arsse::$lang->msg("Driver.User.Internal.Name");
28 18
     }
29 19
 
30
-    public function driverFunctions(string $function = null) {
31
-        if ($function===null) {
32
-            return $this->functions;
20
+    public function auth(string $user, string $password): bool {
21
+        try {
22
+            $hash = $this->userPasswordGet($user);
23
+        } catch (Exception $e) {
24
+            return false;
33 25
         }
34
-        if (array_key_exists($function, $this->functions)) {
35
-            return $this->functions[$function];
36
-        } else {
37
-            return self::FUNC_NOT_IMPLEMENTED;
26
+        if ($password==="" && $hash==="") {
27
+            return true;
28
+        }
29
+        return password_verify($password, $hash);
30
+    }
31
+
32
+    public function authorize(string $affectedUser, string $action): bool {
33
+        return true;
34
+    }
35
+
36
+    public function userExists(string $user): bool {
37
+        return Arsse::$db->userExists($user);
38
+    }
39
+
40
+    public function userAdd(string $user, string $password = null) {
41
+        if (isset($password)) {
42
+            // only add the user if the password is not null; the user manager will retry with a generated password if null is returned
43
+            Arsse::$db->userAdd($user, $password);
38 44
         }
45
+        return $password;
46
+    }
47
+
48
+    public function userRemove(string $user): bool {
49
+        return Arsse::$db->userRemove($user);
50
+    }
51
+
52
+    public function userList(): array {
53
+        return Arsse::$db->userList();
54
+    }
55
+
56
+    public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null) {
57
+        // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception)
58
+        return $newPassword;
39 59
     }
40 60
 
41
-    // see InternalFunctions.php for bulk of methods
61
+    protected function userPasswordGet(string $user): string {
62
+        return Arsse::$db->userPasswordGet($user);
63
+    }
42 64
 }

+ 0
- 65
lib/User/Internal/InternalFunctions.php View File

@@ -1,65 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\User\Internal;
8
-
9
-use JKingWeb\Arsse\Arsse;
10
-use JKingWeb\Arsse\User\Exception;
11
-
12
-trait InternalFunctions {
13
-    protected $actor = [];
14
-
15
-    public function __construct() {
16
-    }
17
-
18
-    public function auth(string $user, string $password): bool {
19
-        try {
20
-            $hash = Arsse::$db->userPasswordGet($user);
21
-        } catch (Exception $e) {
22
-            return false;
23
-        }
24
-        if ($password==="" && $hash==="") {
25
-            return true;
26
-        }
27
-        return password_verify($password, $hash);
28
-    }
29
-
30
-    public function userExists(string $user): bool {
31
-        return Arsse::$db->userExists($user);
32
-    }
33
-
34
-    public function userAdd(string $user, string $password = null): string {
35
-        return Arsse::$db->userAdd($user, $password);
36
-    }
37
-
38
-    public function userRemove(string $user): bool {
39
-        return Arsse::$db->userRemove($user);
40
-    }
41
-
42
-    public function userList(string $domain = null): array {
43
-        return Arsse::$db->userList($domain);
44
-    }
45
-
46
-    public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
47
-        return Arsse::$db->userPasswordSet($user, $newPassword);
48
-    }
49
-
50
-    public function userPropertiesGet(string $user): array {
51
-        return Arsse::$db->userPropertiesGet($user);
52
-    }
53
-
54
-    public function userPropertiesSet(string $user, array $properties): array {
55
-        return Arsse::$db->userPropertiesSet($user, $properties);
56
-    }
57
-
58
-    public function userRightsGet(string $user): int {
59
-        return Arsse::$db->userRightsGet($user);
60
-    }
61
-
62
-    public function userRightsSet(string $user, int $level): bool {
63
-        return Arsse::$db->userRightsSet($user, $level);
64
-    }
65
-}

+ 1
- 4
locale/en.php View File

@@ -166,10 +166,7 @@ return [
166 166
     'Exception.JKingWeb/Arsse/User/Exception.authFailed'                   => 'Authentication failed',
167 167
     'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized'           =>
168 168
         '{action, select,
169
-            userList {{user, select,
170
-                global {Authenticated user is not authorized to view the global user list}
171
-                other {Authenticated user is not authorized to view the user list for domain {user}}
172
-            }}
169
+            userList {Authenticated user is not authorized to view the user list}
173 170
             other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}}
174 171
         }',
175 172
     'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid'               => 'Session with ID {0} does not exist',

+ 12
- 18
tests/cases/REST/NextCloudNews/TestV1_2.php View File

@@ -314,7 +314,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
314 314
             $server['HTTP_CONTENT_TYPE'] = "application/json";
315 315
         }
316 316
         $req = new ServerRequest($server, [], $url, $method, "php://memory");
317
-        if (Arsse::$user->auth()) {
317
+        if (Arsse::$user->auth("john.doe@example.com", "secret")) {
318 318
             $req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", "john.doe@example.com");
319 319
         }
320 320
         foreach ($headers as $key => $value) {
@@ -344,7 +344,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
344 344
         // create a mock user manager
345 345
         Arsse::$user = Phake::mock(User::class);
346 346
         Phake::when(Arsse::$user)->auth->thenReturn(true);
347
-        Phake::when(Arsse::$user)->rightsGet->thenReturn(100);
348 347
         Arsse::$user->id = "john.doe@example.com";
349 348
         // create a mock database interface
350 349
         Arsse::$db = Phake::mock(Database::class);
@@ -696,10 +695,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
696 695
         Phake::when(Arsse::$db)->feedListStale->thenReturn($this->v(array_column($out, "id")));
697 696
         $exp = new Response(['feeds' => $out]);
698 697
         $this->assertMessage($exp, $this->req("GET", "/feeds/all"));
699
-        // retrieving the list when not an admin fails
700
-        Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
701
-        $exp = new EmptyResponse(403);
702
-        $this->assertMessage($exp, $this->req("GET", "/feeds/all"));
703 698
     }
704 699
 
705 700
     public function testUpdateAFeed() {
@@ -721,10 +716,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
721 716
         $this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[2])));
722 717
         $this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[3])));
723 718
         $this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[4])));
724
-        // updating a feed when not an admin fails
725
-        Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
726
-        $exp = new EmptyResponse(403);
727
-        $this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[0])));
728 719
     }
729 720
 
730 721
     public function testListArticles() {
@@ -929,10 +920,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
929 920
         $exp = new EmptyResponse(204);
930 921
         $this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
931 922
         Phake::verify(Arsse::$db)->feedCleanup();
932
-        // performing a cleanup when not an admin fails
933
-        Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
934
-        $exp = new EmptyResponse(403);
935
-        $this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
936 923
     }
937 924
 
938 925
     public function testCleanUpAfterUpdate() {
@@ -940,9 +927,16 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
940 927
         $exp = new EmptyResponse(204);
941 928
         $this->assertMessage($exp, $this->req("GET", "/cleanup/after-update"));
942 929
         Phake::verify(Arsse::$db)->articleCleanup();
943
-        // performing a cleanup when not an admin fails
944
-        Phake::when(Arsse::$user)->rightsGet->thenReturn(0);
945
-        $exp = new EmptyResponse(403);
946
-        $this->assertMessage($exp, $this->req("GET", "/cleanup/after-update"));
930
+    }
931
+
932
+    public function testQueryTheUserStatus() {
933
+        $act = $this->req("GET", "/user");
934
+        $exp = new Response([
935
+            'userId' => Arsse::$user->id,
936
+            'displayName' => Arsse::$user->id,
937
+            'lastLoginTimestamp' => $this->approximateTime($act->getPayload()['lastLoginTimestamp'], new \DateTimeImmutable),
938
+            'avatar' => null,
939
+        ]);
940
+        $this->assertMessage($exp, $act);
947 941
     }
948 942
 }

+ 0
- 1
tests/cases/REST/TinyTinyRSS/TestAPI.php View File

@@ -181,7 +181,6 @@ LONG_STRING;
181 181
         // create a mock user manager
182 182
         Arsse::$user = Phake::mock(User::class);
183 183
         Phake::when(Arsse::$user)->auth->thenReturn(true);
184
-        Phake::when(Arsse::$user)->rightsGet->thenReturn(100);
185 184
         Arsse::$user->id = "john.doe@example.com";
186 185
         // create a mock database interface
187 186
         Arsse::$db = Phake::mock(Database::class);

+ 0
- 338
tests/cases/User/TestAuthorization.php View File

@@ -1,338 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\TestCase\User;
8
-
9
-use JKingWeb\Arsse\Arsse;
10
-use JKingWeb\Arsse\Conf;
11
-use JKingWeb\Arsse\User;
12
-use JKingWeb\Arsse\User\Driver;
13
-use Phake;
14
-
15
-/** @covers \JKingWeb\Arsse\User */
16
-class TestAuthorization extends \JKingWeb\Arsse\Test\AbstractTest {
17
-    const USERS = [
18
-        'user@example.com' => Driver::RIGHTS_NONE,
19
-        'user@example.org' => Driver::RIGHTS_NONE,
20
-        'dman@example.com' => Driver::RIGHTS_DOMAIN_MANAGER,
21
-        'dman@example.org' => Driver::RIGHTS_DOMAIN_MANAGER,
22
-        'dadm@example.com' => Driver::RIGHTS_DOMAIN_ADMIN,
23
-        'dadm@example.org' => Driver::RIGHTS_DOMAIN_ADMIN,
24
-        'gman@example.com' => Driver::RIGHTS_GLOBAL_MANAGER,
25
-        'gman@example.org' => Driver::RIGHTS_GLOBAL_MANAGER,
26
-        'gadm@example.com' => Driver::RIGHTS_GLOBAL_ADMIN,
27
-        'gadm@example.org' => Driver::RIGHTS_GLOBAL_ADMIN,
28
-        // invalid rights levels
29
-        'bad1@example.com' => Driver::RIGHTS_NONE+1,
30
-        'bad1@example.org' => Driver::RIGHTS_NONE+1,
31
-        'bad2@example.com' => Driver::RIGHTS_DOMAIN_MANAGER+1,
32
-        'bad2@example.org' => Driver::RIGHTS_DOMAIN_MANAGER+1,
33
-        'bad3@example.com' => Driver::RIGHTS_DOMAIN_ADMIN+1,
34
-        'bad3@example.org' => Driver::RIGHTS_DOMAIN_ADMIN+1,
35
-        'bad4@example.com' => Driver::RIGHTS_GLOBAL_MANAGER+1,
36
-        'bad4@example.org' => Driver::RIGHTS_GLOBAL_MANAGER+1,
37
-        'bad5@example.com' => Driver::RIGHTS_GLOBAL_ADMIN+1,
38
-        'bad5@example.org' => Driver::RIGHTS_GLOBAL_ADMIN+1,
39
-
40
-    ];
41
-    const LEVELS = [
42
-        Driver::RIGHTS_NONE,
43
-        Driver::RIGHTS_DOMAIN_MANAGER,
44
-        Driver::RIGHTS_DOMAIN_ADMIN,
45
-        Driver::RIGHTS_GLOBAL_MANAGER,
46
-        Driver::RIGHTS_GLOBAL_ADMIN,
47
-    ];
48
-    const DOMAINS = [
49
-        '@example.com',
50
-        '@example.org',
51
-        "",
52
-    ];
53
-
54
-    protected $data;
55
-
56
-    public function setUp(string $drv = \JkingWeb\Arsse\Test\User\DriverInternalMock::class, string $db = null) {
57
-        $this->clearData();
58
-        $conf = new Conf();
59
-        $conf->userDriver = $drv;
60
-        $conf->userPreAuth = false;
61
-        Arsse::$conf = $conf;
62
-        if ($db !== null) {
63
-            Arsse::$db = new $db();
64
-        }
65
-        Arsse::$user = Phake::partialMock(User::class);
66
-        Phake::when(Arsse::$user)->authorize->thenReturn(true);
67
-        foreach (self::USERS as $user => $level) {
68
-            Arsse::$user->add($user, "");
69
-            Arsse::$user->rightsSet($user, $level);
70
-        }
71
-        Phake::reset(Arsse::$user);
72
-    }
73
-
74
-    public function tearDown() {
75
-        $this->clearData();
76
-    }
77
-
78
-    public function testToggleLogic() {
79
-        $this->assertTrue(Arsse::$user->authorizationEnabled());
80
-        $this->assertTrue(Arsse::$user->authorizationEnabled(true));
81
-        $this->assertFalse(Arsse::$user->authorizationEnabled(false));
82
-        $this->assertFalse(Arsse::$user->authorizationEnabled(false));
83
-        $this->assertFalse(Arsse::$user->authorizationEnabled(true));
84
-        $this->assertTrue(Arsse::$user->authorizationEnabled(true));
85
-    }
86
-
87
-    public function testSelfActionLogic() {
88
-        foreach (array_keys(self::USERS) as $user) {
89
-            Arsse::$user->auth($user, "");
90
-            // users should be able to do basic actions for themselves
91
-            $this->assertTrue(Arsse::$user->authorize($user, "userExists"), "User $user could not act for themselves.");
92
-            $this->assertTrue(Arsse::$user->authorize($user, "userRemove"), "User $user could not act for themselves.");
93
-        }
94
-    }
95
-
96
-    public function testRegularUserLogic() {
97
-        foreach (self::USERS as $actor => $rights) {
98
-            if ($rights != Driver::RIGHTS_NONE) {
99
-                continue;
100
-            }
101
-            Arsse::$user->auth($actor, "");
102
-            foreach (array_keys(self::USERS) as $affected) {
103
-                // regular users should only be able to act for themselves
104
-                if ($actor==$affected) {
105
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
106
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
107
-                } else {
108
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
109
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
110
-                }
111
-                // they should never be able to set rights
112
-                foreach (self::LEVELS as $level) {
113
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
114
-                }
115
-            }
116
-            // they should not be able to list users
117
-            foreach (self::DOMAINS as $domain) {
118
-                $this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
119
-            }
120
-        }
121
-    }
122
-
123
-    public function testDomainManagerLogic() {
124
-        foreach (self::USERS as $actor => $actorRights) {
125
-            if ($actorRights != Driver::RIGHTS_DOMAIN_MANAGER) {
126
-                continue;
127
-            }
128
-            $actorDomain = substr($actor, strrpos($actor, "@")+1);
129
-            Arsse::$user->auth($actor, "");
130
-            foreach (self::USERS as $affected => $affectedRights) {
131
-                $affectedDomain = substr($affected, strrpos($affected, "@")+1);
132
-                // domain managers should be able to check any user on the same domain
133
-                if ($actorDomain==$affectedDomain) {
134
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
135
-                } else {
136
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
137
-                }
138
-                // they should only be able to act for regular users on the same domain
139
-                if ($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) {
140
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
141
-                } else {
142
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
143
-                }
144
-                // and they should only be able to set their own rights to regular user
145
-                foreach (self::LEVELS as $level) {
146
-                    if ($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, Driver::RIGHTS_DOMAIN_MANAGER])) {
147
-                        $this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
148
-                    } else {
149
-                        $this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
150
-                    }
151
-                }
152
-            }
153
-            // they should also be able to list all users on their own domain
154
-            foreach (self::DOMAINS as $domain) {
155
-                if ($domain=="@".$actorDomain) {
156
-                    $this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
157
-                } else {
158
-                    $this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
159
-                }
160
-            }
161
-        }
162
-    }
163
-
164
-    public function testDomainAdministratorLogic() {
165
-        foreach (self::USERS as $actor => $actorRights) {
166
-            if ($actorRights != Driver::RIGHTS_DOMAIN_ADMIN) {
167
-                continue;
168
-            }
169
-            $actorDomain = substr($actor, strrpos($actor, "@")+1);
170
-            Arsse::$user->auth($actor, "");
171
-            $allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
172
-            foreach (self::USERS as $affected => $affectedRights) {
173
-                $affectedDomain = substr($affected, strrpos($affected, "@")+1);
174
-                // domain admins should be able to check any user on the same domain
175
-                if ($actorDomain==$affectedDomain) {
176
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
177
-                } else {
178
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
179
-                }
180
-                // they should be able to act for any user on the same domain who is not a global manager or admin
181
-                if ($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) {
182
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
183
-                } else {
184
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
185
-                }
186
-                // they should be able to set rights for any user on their domain who is not a global manager or admin, up to domain admin level
187
-                foreach (self::LEVELS as $level) {
188
-                    if ($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
189
-                        $this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
190
-                    } else {
191
-                        $this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
192
-                    }
193
-                }
194
-            }
195
-            // they should also be able to list all users on their own domain
196
-            foreach (self::DOMAINS as $domain) {
197
-                if ($domain=="@".$actorDomain) {
198
-                    $this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
199
-                } else {
200
-                    $this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
201
-                }
202
-            }
203
-        }
204
-    }
205
-
206
-    public function testGlobalManagerLogic() {
207
-        foreach (self::USERS as $actor => $actorRights) {
208
-            if ($actorRights != Driver::RIGHTS_GLOBAL_MANAGER) {
209
-                continue;
210
-            }
211
-            $actorDomain = substr($actor, strrpos($actor, "@")+1);
212
-            Arsse::$user->auth($actor, "");
213
-            foreach (self::USERS as $affected => $affectedRights) {
214
-                $affectedDomain = substr($affected, strrpos($affected, "@")+1);
215
-                // global managers should be able to check any user
216
-                $this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
217
-                // they should only be able to act for regular users
218
-                if ($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) {
219
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
220
-                } else {
221
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
222
-                }
223
-                // and they should only be able to set their own rights to regular user
224
-                foreach (self::LEVELS as $level) {
225
-                    if ($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, Driver::RIGHTS_GLOBAL_MANAGER])) {
226
-                        $this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
227
-                    } else {
228
-                        $this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
229
-                    }
230
-                }
231
-            }
232
-            // they should also be able to list all users
233
-            foreach (self::DOMAINS as $domain) {
234
-                $this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
235
-            }
236
-        }
237
-    }
238
-
239
-    public function testGlobalAdministratorLogic() {
240
-        foreach (self::USERS as $actor => $actorRights) {
241
-            if ($actorRights != Driver::RIGHTS_GLOBAL_ADMIN) {
242
-                continue;
243
-            }
244
-            Arsse::$user->auth($actor, "");
245
-            // global admins can do anything
246
-            foreach (self::USERS as $affected => $affectedRights) {
247
-                $this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
248
-                $this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
249
-                foreach (self::LEVELS as $level) {
250
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
251
-                }
252
-            }
253
-            foreach (self::DOMAINS as $domain) {
254
-                $this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
255
-            }
256
-        }
257
-    }
258
-
259
-    public function testInvalidLevelLogic() {
260
-        foreach (self::USERS as $actor => $rights) {
261
-            if (in_array($rights, self::LEVELS)) {
262
-                continue;
263
-            }
264
-            Arsse::$user->auth($actor, "");
265
-            foreach (array_keys(self::USERS) as $affected) {
266
-                // users with unknown/invalid rights should be treated just like regular users and only be able to act for themselves
267
-                if ($actor==$affected) {
268
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
269
-                    $this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
270
-                } else {
271
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
272
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
273
-                }
274
-                // they should never be able to set rights
275
-                foreach (self::LEVELS as $level) {
276
-                    $this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
277
-                }
278
-            }
279
-            // they should not be able to list users
280
-            foreach (self::DOMAINS as $domain) {
281
-                $this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
282
-            }
283
-        }
284
-    }
285
-
286
-    public function testInternalExceptionLogic() {
287
-        $tests = [
288
-            // methods of User class to test, with parameters besides affected user
289
-            'exists'        => [],
290
-            'remove'        => [],
291
-            'add'           => [''],
292
-            'passwordSet'   => [''],
293
-            'propertiesGet' => [],
294
-            'propertiesSet' => [[]],
295
-            'rightsGet'     => [],
296
-            'rightsSet'     => [User\Driver::RIGHTS_GLOBAL_ADMIN],
297
-            'list'          => [],
298
-        ];
299
-        // try first with a global admin (there should be no exception)
300
-        Arsse::$user->auth("gadm@example.com", "");
301
-        $this->assertCount(0, $this->checkExceptions("user@example.org", $tests));
302
-        // next try with a regular user acting on another user (everything should fail)
303
-        Arsse::$user->auth("user@example.com", "");
304
-        $this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
305
-    }
306
-
307
-    public function testExternalExceptionLogic() {
308
-        // set up the test for an external driver
309
-        $this->setUp(\JKingWeb\Arsse\Test\User\DriverExternalMock::class, \JKingWeb\Arsse\Test\User\Database::class);
310
-        // run the previous test with the external driver set up
311
-        $this->testInternalExceptionLogic();
312
-    }
313
-
314
-    // meat of testInternalExceptionLogic and testExternalExceptionLogic
315
-    // calls each requested function with supplied arguments, catches authorization exceptions, and returns an array of caught failed calls
316
-    protected function checkExceptions(string $user, $tests): array {
317
-        $err = [];
318
-        foreach ($tests as $func => $args) {
319
-            // list method does not take an affected user, so do not unshift for that one
320
-            if ($func != "list") {
321
-                array_unshift($args, $user);
322
-            }
323
-            try {
324
-                call_user_func_array(array(Arsse::$user, $func), $args);
325
-            } catch (\JKingWeb\Arsse\User\ExceptionAuthz $e) {
326
-                $err[] = $func;
327
-            }
328
-        }
329
-        return $err;
330
-    }
331
-
332
-    public function testMissingUserLogic() {
333
-        Arsse::$user->auth("gadm@example.com", "");
334
-        $this->assertTrue(Arsse::$user->authorize("user@example.com", "someFunction"));
335
-        $this->assertException("doesNotExist", "User");
336
-        Arsse::$user->authorize("this_user_does_not_exist@example.org", "someFunction");
337
-    }
338
-}

+ 137
- 0
tests/cases/User/TestInternal.php View File

@@ -0,0 +1,137 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\User;
8
+
9
+use JKingWeb\Arsse\Arsse;
10
+use JKingWeb\Arsse\Conf;
11
+use JKingWeb\Arsse\Database;
12
+use JKingWeb\Arsse\User;
13
+use JKingWeb\Arsse\AbstractException as Exception;
14
+use JKingWeb\Arsse\User\Driver as DriverInterface;
15
+use JKingWeb\Arsse\User\Internal\Driver;
16
+use Phake;
17
+
18
+/** @covers \JKingWeb\Arsse\User\Internal\Driver */
19
+class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest {
20
+
21
+    public function setUp() {
22
+        $this->clearData();
23
+        $this->setConf();
24
+        // create a mock database interface
25
+        Arsse::$db = Phake::mock(Database::class);
26
+        Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(\JKingWeb\Arsse\Db\Transaction::class));
27
+    }
28
+
29
+    public function testConstruct() {
30
+        $this->assertInstanceOf(DriverInterface::class, new Driver);
31
+    }
32
+
33
+    public function testFetchDriverName() {
34
+        $this->assertTrue(strlen(Driver::driverName()) > 0);
35
+    }
36
+
37
+    /** 
38
+     * @dataProvider provideAuthentication 
39
+     * @group slow
40
+    */
41
+    public function testAuthenticateAUser(bool $authorized, string $user, string $password, bool $exp) {
42
+        if ($authorized) {
43
+            Phake::when(Arsse::$db)->userPasswordGet("john.doe@example.com")->thenReturn('$2y$10$1zbqRJhxM8uUjeSBPp4IhO90xrqK0XjEh9Z16iIYEFRV4U.zeAFom'); // hash of "secret"
44
+            Phake::when(Arsse::$db)->userPasswordGet("jane.doe@example.com")->thenReturn('$2y$10$bK1ljXfTSyc2D.NYvT.Eq..OpehLRXVbglW.23ihVuyhgwJCd.7Im'); // hash of "superman"
45
+            Phake::when(Arsse::$db)->userPasswordGet("owen.hardy@example.com")->thenReturn("");
46
+            Phake::when(Arsse::$db)->userPasswordGet("kira.nerys@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist"));
47
+        } else {
48
+            Phake::when(Arsse::$db)->userPasswordGet->thenThrow(new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized"));
49
+        }
50
+        $this->assertSame($exp, (new Driver)->auth($user, $password));
51
+    }
52
+
53
+    public function provideAuthentication() {
54
+        $john = "john.doe@example.com";
55
+        $jane = "jane.doe@example.com";
56
+        $owen = "owen.hardy@example.com";
57
+        $kira = "kira.nerys@example.com";
58
+        return [
59
+            [false, $john, "secret",     false],
60
+            [false, $jane, "superman",   false],
61
+            [false, $owen, "",           false],
62
+            [false, $kira, "ashalla",    false],
63
+            [true,  $john, "secret",     true],
64
+            [true,  $jane, "superman",   true],
65
+            [true,  $owen, "",           true],
66
+            [true,  $kira, "ashalla",    false],
67
+            [true,  $john, "top secret", false],
68
+            [true,  $jane, "clark kent", false],
69
+            [true,  $owen, "watchmaker", false],
70
+            [true,  $kira, "singha",     false],
71
+            [true,  $john, "",           false],
72
+            [true,  $jane, "",           false],
73
+            [true,  $kira, "",           false],
74
+        ];
75
+    }
76
+
77
+    public function testAuthorizeAnAction() {
78
+        Phake::verifyNoFurtherInteraction(Arsse::$db);
79
+        $this->assertTrue((new Driver)->authorize("someone", "something"));
80
+    }
81
+
82
+    public function testListUsers() {
83
+        $john = "john.doe@example.com";
84
+        $jane = "jane.doe@example.com";
85
+        Phake::when(Arsse::$db)->userList->thenReturn([$john, $jane])->thenReturn([$jane, $john]);
86
+        $driver = new Driver;
87
+        $this->assertSame([$john, $jane], $driver->userList());
88
+        $this->assertSame([$jane, $john], $driver->userList());
89
+        Phake::verify(Arsse::$db, Phake::times(2))->userList;
90
+    }
91
+
92
+    public function testCheckThatAUserExists() {
93
+        $john = "john.doe@example.com";
94
+        $jane = "jane.doe@example.com";
95
+        Phake::when(Arsse::$db)->userExists($john)->thenReturn(true);
96
+        Phake::when(Arsse::$db)->userExists($jane)->thenReturn(false);
97
+        $driver = new Driver;
98
+        $this->assertTrue($driver->userExists($john));
99
+        Phake::verify(Arsse::$db)->userExists($john);
100
+        $this->assertFalse($driver->userExists($jane));
101
+        Phake::verify(Arsse::$db)->userExists($jane);
102
+    }
103
+
104
+    public function testAddAUser() {
105
+        $john = "john.doe@example.com";
106
+        Phake::when(Arsse::$db)->userAdd->thenReturnCallback(function($user, $pass) {
107
+            return $pass;
108
+        });
109
+        $driver = new Driver;
110
+        $this->assertNull($driver->userAdd($john));
111
+        $this->assertNull($driver->userAdd($john, null));
112
+        $this->assertSame("secret", $driver->userAdd($john, "secret"));
113
+        Phake::verify(Arsse::$db)->userAdd($john, "secret");
114
+        Phake::verify(Arsse::$db)->userAdd;
115
+    }
116
+
117
+    public function testRemoveAUser() {
118
+        $john = "john.doe@example.com";
119
+        Phake::when(Arsse::$db)->userRemove->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist"));
120
+        $driver = new Driver;
121
+        $this->assertTrue($driver->userRemove($john));
122
+        Phake::verify(Arsse::$db, Phake::times(1))->userRemove($john);
123
+        $this->assertException("doesNotExist", "User");
124
+        try {
125
+            $this->assertFalse($driver->userRemove($john));
126
+        } finally {
127
+            Phake::verify(Arsse::$db, Phake::times(2))->userRemove($john);
128
+        }
129
+    }
130
+
131
+    public function testSetAPassword() {
132
+        $john = "john.doe@example.com";
133
+        Phake::verifyNoFurtherInteraction(Arsse::$db);
134
+        $this->assertSame("superman", (new Driver)->userPasswordSet($john, "superman"));
135
+        $this->assertSame(null, (new Driver)->userPasswordSet($john, null));
136
+    }
137
+}

+ 0
- 17
tests/cases/User/TestMockExternal.php View File

@@ -1,17 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\TestCase\User;
8
-
9
-/** @covers \JKingWeb\Arsse\User */
10
-class TestMockExternal extends \JKingWeb\Arsse\Test\AbstractTest {
11
-    use \JKingWeb\Arsse\Test\User\CommonTests;
12
-
13
-    const USER1 = "john.doe@example.com";
14
-    const USER2 = "jane.doe@example.com";
15
-
16
-    public $drv = \JKingWeb\Arsse\Test\User\DriverExternalMock::class;
17
-}

+ 0
- 23
tests/cases/User/TestMockInternal.php View File

@@ -1,23 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\TestCase\User;
8
-
9
-use JKingWeb\Arsse\Arsse;
10
-
11
-/** @covers \JKingWeb\Arsse\User */
12
-class TestMockInternal extends \JKingWeb\Arsse\Test\AbstractTest {
13
-    use \JKingWeb\Arsse\Test\User\CommonTests;
14
-
15
-    const USER1 = "john.doe@example.com";
16
-    const USER2 = "jane.doe@example.com";
17
-
18
-    public $drv = \JKingWeb\Arsse\Test\User\DriverInternalMock::class;
19
-
20
-    public function setUpSeries() {
21
-        Arsse::$db = null;
22
-    }
23
-}

+ 301
- 0
tests/cases/User/TestUser.php View File

@@ -0,0 +1,301 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\TestCase\User;
8
+
9
+use JKingWeb\Arsse\Arsse;
10
+use JKingWeb\Arsse\Conf;
11
+use JKingWeb\Arsse\Database;
12
+use JKingWeb\Arsse\User;
13
+use JKingWeb\Arsse\AbstractException as Exception;
14
+use JKingWeb\Arsse\User\Driver;
15
+use JKingWeb\Arsse\User\Internal\Driver as InternalDriver;
16
+use Phake;
17
+
18
+/** @covers \JKingWeb\Arsse\User */
19
+class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
20
+
21
+    public function setUp() {
22
+        $this->clearData();
23
+        $this->setConf();
24
+        // create a mock database interface
25
+        Arsse::$db = Phake::mock(Database::class);
26
+        Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(\JKingWeb\Arsse\Db\Transaction::class));
27
+        // create a mock user driver
28
+        $this->drv = Phake::mock(Driver::class);
29
+    }
30
+
31
+    public function testListDrivers() {
32
+        $exp = [
33
+            'JKingWeb\\Arsse\\User\\Internal\\Driver' => Arsse::$lang->msg("Driver.User.Internal.Name"),
34
+        ];
35
+        $this->assertArraySubset($exp, User::driverList());
36
+    }
37
+
38
+    public function testConstruct() {
39
+        $this->assertInstanceOf(User::class, new User($this->drv));
40
+        $this->assertInstanceOf(User::class, new User);
41
+    }
42
+
43
+    public function testConversionToString() {
44
+        $u = new User;
45
+        $u->id = "john.doe@example.com";
46
+        $this->assertSame("john.doe@example.com", (string) $u);
47
+        $u->id = null;
48
+        $this->assertSame("", (string) $u);
49
+    }
50
+
51
+    /** @dataProvider provideAuthentication */
52
+    public function testAuthenticateAUser(bool $preAuth, string $user, string $password, bool $exp) {
53
+        Arsse::$conf->userPreAuth = $preAuth;
54
+        Phake::when($this->drv)->auth->thenReturn(false);
55
+        Phake::when($this->drv)->auth("john.doe@example.com", "secret")->thenReturn(true);
56
+        Phake::when($this->drv)->auth("jane.doe@example.com", "superman")->thenReturn(true);
57
+        Phake::when(Arsse::$db)->userExists("john.doe@example.com")->thenReturn(true);
58
+        Phake::when(Arsse::$db)->userExists("jane.doe@example.com")->thenReturn(false);
59
+        Phake::when(Arsse::$db)->userAdd->thenReturn("");
60
+        $u = new User($this->drv);
61
+        $this->assertSame($exp, $u->auth($user, $password));
62
+        $this->assertNull($u->id);
63
+        Phake::verify(Arsse::$db, Phake::times($exp ? 1 : 0))->userExists($user);
64
+        Phake::verify(Arsse::$db, Phake::times($exp && $user == "jane.doe@example.com" ? 1 : 0))->userAdd($user, $password);
65
+    }
66
+
67
+    public function provideAuthentication() {
68
+        $john = "john.doe@example.com";
69
+        $jane = "jane.doe@example.com";
70
+        return [
71
+            [false, $john, "secret",   true],
72
+            [false, $john, "superman", false],
73
+            [false, $jane, "secret",   false],
74
+            [false, $jane, "superman", true],
75
+            [true,  $john, "secret",   true],
76
+            [true,  $john, "superman", true],
77
+            [true,  $jane, "secret",   true],
78
+            [true,  $jane, "superman", true],
79
+        ];
80
+    }
81
+
82
+    /** @dataProvider provideUserList */
83
+    public function testListUsers(bool $authorized, $exp) {
84
+        $u = new User($this->drv);
85
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
86
+        Phake::when($this->drv)->userList->thenReturn(["john.doe@example.com", "jane.doe@example.com"]);
87
+        if ($exp instanceof Exception) {
88
+            $this->assertException("notAuthorized", "User", "ExceptionAuthz");
89
+        }
90
+        $this->assertSame($exp, $u->list());
91
+    }
92
+
93
+    public function provideUserList() {
94
+        $john = "john.doe@example.com";
95
+        $jane = "jane.doe@example.com";
96
+        return [
97
+            [false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
98
+            [true,  [$john, $jane]],
99
+        ];
100
+    }
101
+
102
+    /** @dataProvider provideExistence */
103
+    public function testCheckThatAUserExists(bool $authorized, string $user, $exp) {
104
+        $u = new User($this->drv);
105
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
106
+        Phake::when($this->drv)->userExists("john.doe@example.com")->thenReturn(true);
107
+        Phake::when($this->drv)->userExists("jane.doe@example.com")->thenReturn(false);
108
+        if ($exp instanceof Exception) {
109
+            $this->assertException("notAuthorized", "User", "ExceptionAuthz");
110
+        }
111
+        $this->assertSame($exp, $u->exists($user));
112
+    }
113
+
114
+    public function provideExistence() {
115
+        $john = "john.doe@example.com";
116
+        $jane = "jane.doe@example.com";
117
+        return [
118
+            [false, $john, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
119
+            [false, $jane, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
120
+            [true,  $john, true],
121
+            [true,  $jane, false],
122
+        ];
123
+    }
124
+
125
+    /** @dataProvider provideAdditions */
126
+    public function testAddAUser(bool $authorized, string $user, $password, $exp) {
127
+        $u = new User($this->drv);
128
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
129
+        Phake::when($this->drv)->userAdd("john.doe@example.com", $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("alreadyExists"));
130
+        Phake::when($this->drv)->userAdd("jane.doe@example.com", $this->anything())->thenReturnCallback(function($user, $pass) {
131
+            return $pass ?? "random password";
132
+        });
133
+        if ($exp instanceof Exception) {
134
+            if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) {
135
+                $this->assertException("notAuthorized", "User", "ExceptionAuthz");
136
+            } else {
137
+                $this->assertException("alreadyExists", "User");
138
+            }
139
+        }
140
+        $this->assertSame($exp, $u->add($user, $password));
141
+    }
142
+
143
+    /** @dataProvider provideAdditions */
144
+    public function testAddAUserWithARandomPassword(bool $authorized, string $user, $password, $exp) {
145
+        $u = Phake::partialMock(User::class, $this->drv);
146
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
147
+        Phake::when($this->drv)->userAdd($this->anything(), $this->isNull())->thenReturn(null);
148
+        Phake::when($this->drv)->userAdd("john.doe@example.com", $this->logicalNot($this->isNull()))->thenThrow(new \JKingWeb\Arsse\User\Exception("alreadyExists"));
149
+        Phake::when($this->drv)->userAdd("jane.doe@example.com", $this->logicalNot($this->isNull()))->thenReturnCallback(function($user, $pass) {
150
+            return $pass;
151
+        });
152
+        if ($exp instanceof Exception) {
153
+            if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) {
154
+                $this->assertException("notAuthorized", "User", "ExceptionAuthz");
155
+                $calls = 0;
156
+            } else {
157
+                $this->assertException("alreadyExists", "User");
158
+                $calls = 2;
159
+            }
160
+        } else {
161
+            $calls =  4;
162
+        }
163
+        try {
164
+            $pass1 = $u->add($user, null);
165
+            $pass2 = $u->add($user, null);
166
+            $this->assertNotEquals($pass1, $pass2);
167
+        } finally {
168
+            Phake::verify($this->drv, Phake::times($calls))->userAdd;
169
+            Phake::verify($u, Phake::times($calls / 2))->generatePassword;
170
+        }
171
+    }
172
+
173
+    public function provideAdditions() {
174
+        $john = "john.doe@example.com";
175
+        $jane = "jane.doe@example.com";
176
+        return [
177
+            [false, $john, "secret",   new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
178
+            [false, $jane, "superman", new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
179
+            [true,  $john, "secret",   new \JKingWeb\Arsse\User\Exception("alreadyExists")],
180
+            [true,  $jane, "superman", "superman"],
181
+            [true,  $jane, null,       "random password"],
182
+        ];
183
+    }
184
+
185
+    /** @dataProvider provideRemovals */
186
+    public function testRemoveAUser(bool $authorized, string $user, bool $exists, $exp) {
187
+        $u = new User($this->drv);
188
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
189
+        Phake::when($this->drv)->userRemove("john.doe@example.com")->thenReturn(true);
190
+        Phake::when($this->drv)->userRemove("jane.doe@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist"));
191
+        Phake::when(Arsse::$db)->userExists->thenReturn($exists);
192
+        Phake::when(Arsse::$db)->userRemove->thenReturn(true);
193
+        if ($exp instanceof Exception) {
194
+            if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) {
195
+                $this->assertException("notAuthorized", "User", "ExceptionAuthz");
196
+            } else {
197
+                $this->assertException("doesNotExist", "User");
198
+            }
199
+        }
200
+        try {
201
+            $this->assertSame($exp, $u->remove($user));
202
+        } finally {
203
+            Phake::verify(Arsse::$db, Phake::times((int) $authorized))->userExists($user);
204
+            Phake::verify(Arsse::$db, Phake::times((int) ($authorized && $exists)))->userRemove($user);
205
+        }
206
+    }
207
+
208
+    public function provideRemovals() {
209
+        $john = "john.doe@example.com";
210
+        $jane = "jane.doe@example.com";
211
+        return [
212
+            [false, $john, true,  new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
213
+            [false, $john, false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
214
+            [false, $jane, true,  new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
215
+            [false, $jane, false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
216
+            [true,  $john, true,  true],
217
+            [true,  $john, false, true],
218
+            [true,  $jane, true,  new \JKingWeb\Arsse\User\Exception("doesNotExist")],
219
+            [true,  $jane, false, new \JKingWeb\Arsse\User\Exception("doesNotExist")],
220
+        ];
221
+    }
222
+
223
+    /** @dataProvider providePasswordChanges */
224
+    public function testChangeAPassword(bool $authorized, string $user, $password, bool $exists, $exp) {
225
+        $u = new User($this->drv);
226
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
227
+        Phake::when($this->drv)->userPasswordSet("john.doe@example.com", $this->anything(), $this->anything())->thenReturnCallback(function($user, $pass, $old) {
228
+            return $pass ?? "random password";
229
+        });
230
+        Phake::when($this->drv)->userPasswordSet("jane.doe@example.com", $this->anything(), $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist"));
231
+        Phake::when(Arsse::$db)->userExists->thenReturn($exists);
232
+        if ($exp instanceof Exception) {
233
+            if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) {
234
+                $this->assertException("notAuthorized", "User", "ExceptionAuthz");
235
+            } else {
236
+                $this->assertException("doesNotExist", "User");
237
+            }
238
+            $calls = 0;
239
+        } else{
240
+            $calls = 1;
241
+        }
242
+        try {
243
+            $this->assertSame($exp, $u->passwordSet($user, $password));
244
+        } finally {
245
+            Phake::verify(Arsse::$db, Phake::times($calls))->userExists($user);
246
+            Phake::verify(Arsse::$db, Phake::times($exists ? $calls : 0))->userPasswordSet($user, $password ?? "random password", null);
247
+        }
248
+    }
249
+
250
+    /** @dataProvider providePasswordChanges */
251
+    public function testChangeAPasswordToARandomPassword(bool $authorized, string $user, $password, bool $exists, $exp) {
252
+        $u = Phake::partialMock(User::class, $this->drv);
253
+        Phake::when($this->drv)->authorize->thenReturn($authorized);
254
+        Phake::when($this->drv)->userPasswordSet($this->anything(), $this->isNull(), $this->anything())->thenReturn(null);
255
+        Phake::when($this->drv)->userPasswordSet("john.doe@example.com", $this->logicalNot($this->isNull()), $this->anything())->thenReturnCallback(function($user, $pass, $old) {
256
+            return $pass ?? "random password";
257
+        });
258
+        Phake::when($this->drv)->userPasswordSet("jane.doe@example.com", $this->logicalNot($this->isNull()), $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist"));
259
+        Phake::when(Arsse::$db)->userExists->thenReturn($exists);
260
+        if ($exp instanceof Exception) {
261
+            if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) {
262
+                $this->assertException("notAuthorized", "User", "ExceptionAuthz");
263
+                $calls = 0;
264
+            } else {
265
+                $this->assertException("doesNotExist", "User");
266
+                $calls = 2;
267
+            }
268
+        } else {
269
+            $calls =  4;
270
+        }
271
+        try {
272
+            $pass1 = $u->passwordSet($user, null);
273
+            $pass2 = $u->passwordSet($user, null);
274
+            $this->assertNotEquals($pass1, $pass2);
275
+        } finally {
276
+            Phake::verify($this->drv, Phake::times($calls))->userPasswordSet;
277
+            Phake::verify($u, Phake::times($calls / 2))->generatePassword;
278
+            Phake::verify(Arsse::$db, Phake::times($calls==4 ? 2 : 0))->userExists($user);
279
+            if ($calls == 4) {
280
+                Phake::verify(Arsse::$db, Phake::times($exists ? 1 : 0))->userPasswordSet($user, $pass1, null);
281
+                Phake::verify(Arsse::$db, Phake::times($exists ? 1 : 0))->userPasswordSet($user, $pass2, null);
282
+            } else {
283
+                Phake::verify(Arsse::$db, Phake::times(0))->userPasswordSet;
284
+            }
285
+        }
286
+    }
287
+
288
+    public function providePasswordChanges() {
289
+        $john = "john.doe@example.com";
290
+        $jane = "jane.doe@example.com";
291
+        return [
292
+            [false, $john, "secret",   true,  new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
293
+            [false, $jane, "superman", false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")],
294
+            [true,  $john, "superman", true,  "superman"],
295
+            [true,  $john, null,       true,  "random password"],
296
+            [true,  $john, "superman", false, "superman"],
297
+            [true,  $john, null,       false, "random password"],
298
+            [true,  $jane, "secret",   true,  new \JKingWeb\Arsse\User\Exception("doesNotExist")],
299
+        ];
300
+    }
301
+}

+ 0
- 20
tests/cases/User/Testnternal.php View File

@@ -1,20 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\TestCase\User;
8
-
9
-/**
10
- * @covers \JKingWeb\Arsse\User
11
- * @covers \JKingWeb\Arsse\User\Internal\Driver
12
- * @covers \JKingWeb\Arsse\User\Internal\InternalFunctions */
13
-class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest {
14
-    use \JKingWeb\Arsse\Test\User\CommonTests;
15
-
16
-    const USER1 = "john.doe@example.com";
17
-    const USER2 = "jane.doe@example.com";
18
-
19
-    public $drv = \JKingWeb\Arsse\User\Internal\Driver::class;
20
-}

+ 7
- 147
tests/lib/Database/SeriesUser.php View File

@@ -20,9 +20,9 @@ trait SeriesUser {
20 20
                 'rights'   => 'int',
21 21
             ],
22 22
             'rows' => [
23
-                ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", UserDriver::RIGHTS_GLOBAL_ADMIN], // password is hash of "secret"
24
-                ["jane.doe@example.com", "", "Jane Doe", UserDriver::RIGHTS_NONE],
25
-                ["john.doe@example.com", "", "John Doe", UserDriver::RIGHTS_NONE],
23
+                ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", 100], // password is hash of "secret"
24
+                ["jane.doe@example.com", "", "Jane Doe", 0],
25
+                ["john.doe@example.com", "", "John Doe", 0],
26 26
             ],
27 27
         ],
28 28
     ];
@@ -60,35 +60,13 @@ trait SeriesUser {
60 60
     }
61 61
 
62 62
     public function testAddANewUser() {
63
-        $this->assertSame("", Arsse::$db->userAdd("john.doe@example.org", ""));
63
+        $this->assertTrue(Arsse::$db->userAdd("john.doe@example.org", ""));
64 64
         Phake::verify(Arsse::$user)->authorize("john.doe@example.org", "userAdd");
65 65
         $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','name','rights']]);
66
-        $state['arsse_users']['rows'][] = ["john.doe@example.org", null, UserDriver::RIGHTS_NONE];
66
+        $state['arsse_users']['rows'][] = ["john.doe@example.org", null, 0];
67 67
         $this->compareExpectations($state);
68 68
     }
69 69
 
70
-    /**
71
-     * @depends testGetAPassword
72
-     * @depends testAddANewUser
73
-     */
74
-    public function testAddANewUserWithARandomPassword() {
75
-        $user1 = "john.doe@example.org";
76
-        $user2 = "john.doe@example.net";
77
-        $pass1 = Arsse::$db->userAdd($user1);
78
-        $pass2 = Arsse::$db->userAdd($user2);
79
-        $this->assertSame(Arsse::$conf->userTempPasswordLength, strlen($pass1));
80
-        $this->assertSame(Arsse::$conf->userTempPasswordLength, strlen($pass2));
81
-        $this->assertNotEquals($pass1, $pass2);
82
-        $hash1 = Arsse::$db->userPasswordGet($user1);
83
-        $hash2 = Arsse::$db->userPasswordGet($user2);
84
-        Phake::verify(Arsse::$user)->authorize($user1, "userAdd");
85
-        Phake::verify(Arsse::$user)->authorize($user2, "userAdd");
86
-        Phake::verify(Arsse::$user)->authorize($user1, "userPasswordGet");
87
-        Phake::verify(Arsse::$user)->authorize($user2, "userPasswordGet");
88
-        $this->assertTrue(password_verify($pass1, $hash1), "Failed verifying password of $user1 '$pass1' against hash '$hash1'.");
89
-        $this->assertTrue(password_verify($pass2, $hash2), "Failed verifying password of $user2 '$pass2' against hash '$hash2'.");
90
-    }
91
-
92 70
     public function testAddAnExistingUser() {
93 71
         $this->assertException("alreadyExists", "User");
94 72
         Arsse::$db->userAdd("john.doe@example.com", "");
@@ -125,42 +103,25 @@ trait SeriesUser {
125 103
         Phake::verify(Arsse::$user)->authorize("", "userList");
126 104
     }
127 105
 
128
-    public function testListUsersOnADomain() {
129
-        $users = ["jane.doe@example.com", "john.doe@example.com"];
130
-        $this->assertSame($users, Arsse::$db->userList("example.com"));
131
-        Phake::verify(Arsse::$user)->authorize("@example.com", "userList");
132
-    }
133
-
134 106
     public function testListAllUsersWithoutAuthority() {
135 107
         Phake::when(Arsse::$user)->authorize->thenReturn(false);
136 108
         $this->assertException("notAuthorized", "User", "ExceptionAuthz");
137 109
         Arsse::$db->userList();
138 110
     }
139 111
 
140
-    public function testListUsersOnADomainWithoutAuthority() {
141
-        Phake::when(Arsse::$user)->authorize->thenReturn(false);
142
-        $this->assertException("notAuthorized", "User", "ExceptionAuthz");
143
-        Arsse::$db->userList("example.com");
144
-    }
145
-
146 112
     /**
147 113
      * @depends testGetAPassword
148 114
      */
149 115
     public function testSetAPassword() {
150 116
         $user = "john.doe@example.com";
117
+        $pass = "secret";
151 118
         $this->assertEquals("", Arsse::$db->userPasswordGet($user));
152
-        $pass = Arsse::$db->userPasswordSet($user, "secret");
119
+        $this->assertTrue(Arsse::$db->userPasswordSet($user, $pass));
153 120
         $hash = Arsse::$db->userPasswordGet($user);
154 121
         $this->assertNotEquals("", $hash);
155 122
         Phake::verify(Arsse::$user)->authorize($user, "userPasswordSet");
156 123
         $this->assertTrue(password_verify($pass, $hash), "Failed verifying password of $user '$pass' against hash '$hash'.");
157 124
     }
158
-    public function testSetARandomPassword() {
159
-        $user = "john.doe@example.com";
160
-        $this->assertEquals("", Arsse::$db->userPasswordGet($user));
161
-        $pass = Arsse::$db->userPasswordSet($user);
162
-        $hash = Arsse::$db->userPasswordGet($user);
163
-    }
164 125
 
165 126
     public function testSetThePasswordOfAMissingUser() {
166 127
         $this->assertException("doesNotExist", "User");
@@ -172,105 +133,4 @@ trait SeriesUser {
172 133
         $this->assertException("notAuthorized", "User", "ExceptionAuthz");
173 134
         Arsse::$db->userPasswordSet("john.doe@example.com", "secret");
174 135
     }
175
-
176
-    public function testGetUserProperties() {
177
-        $exp = [
178
-            'name'   => 'Hard Lip Herbert',
179
-            'rights' => UserDriver::RIGHTS_GLOBAL_ADMIN,
180
-        ];
181
-        $props = Arsse::$db->userPropertiesGet("admin@example.net");
182
-        Phake::verify(Arsse::$user)->authorize("admin@example.net", "userPropertiesGet");
183
-        $this->assertArraySubset($exp, $props);
184
-        $this->assertArrayNotHasKey("password", $props);
185
-    }
186
-
187
-    public function testGetThePropertiesOfAMissingUser() {
188
-        $this->assertException("doesNotExist", "User");
189
-        Arsse::$db->userPropertiesGet("john.doe@example.org");
190
-    }
191
-
192
-    public function testGetUserPropertiesWithoutAuthority() {
193
-        Phake::when(Arsse::$user)->authorize->thenReturn(false);
194
-        $this->assertException("notAuthorized", "User", "ExceptionAuthz");
195
-        Arsse::$db->userPropertiesGet("john.doe@example.com");
196
-    }
197
-
198
-    public function testSetUserProperties() {
199
-        $try = [
200
-            'name'     => 'James Kirk', // only this should actually change
201
-            'password' => '000destruct0',
202
-            'rights'   => UserDriver::RIGHTS_NONE,
203
-            'lifeform' => 'tribble',
204
-        ];
205
-        $exp = [
206
-            'name'   => 'James Kirk',
207
-            'rights' => UserDriver::RIGHTS_GLOBAL_ADMIN,
208
-        ];
209
-        $props = Arsse::$db->userPropertiesSet("admin@example.net", $try);
210
-        Phake::verify(Arsse::$user)->authorize("admin@example.net", "userPropertiesSet");
211
-        $this->assertArraySubset($exp, $props);
212
-        $this->assertArrayNotHasKey("password", $props);
213
-        $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','password','name','rights']]);
214
-        $state['arsse_users']['rows'][0][2] = "James Kirk";
215
-        $this->compareExpectations($state);
216
-        // making now changes should make no changes :)
217
-        Arsse::$db->userPropertiesSet("admin@example.net", ['lifeform' => "tribble"]);
218
-        $this->compareExpectations($state);
219
-    }
220
-
221
-    public function testSetThePropertiesOfAMissingUser() {
222
-        $try = ['name' => 'John Doe'];
223
-        $this->assertException("doesNotExist", "User");
224
-        Arsse::$db->userPropertiesSet("john.doe@example.org", $try);
225
-    }
226
-
227
-    public function testSetUserPropertiesWithoutAuthority() {
228
-        $try = ['name' => 'John Doe'];
229
-        Phake::when(Arsse::$user)->authorize->thenReturn(false);
230
-        $this->assertException("notAuthorized", "User", "ExceptionAuthz");
231
-        Arsse::$db->userPropertiesSet("john.doe@example.com", $try);
232
-    }
233
-
234
-    public function testGetUserRights() {
235
-        $user1 = "john.doe@example.com";
236
-        $user2 = "admin@example.net";
237
-        $this->assertSame(UserDriver::RIGHTS_NONE, Arsse::$db->userRightsGet($user1));
238
-        $this->assertSame(UserDriver::RIGHTS_GLOBAL_ADMIN, Arsse::$db->userRightsGet($user2));
239
-        Phake::verify(Arsse::$user)->authorize($user1, "userRightsGet");
240
-        Phake::verify(Arsse::$user)->authorize($user2, "userRightsGet");
241
-    }
242
-
243
-    public function testGetTheRightsOfAMissingUser() {
244
-        $this->assertSame(UserDriver::RIGHTS_NONE, Arsse::$db->userRightsGet("john.doe@example.org"));
245
-        Phake::verify(Arsse::$user)->authorize("john.doe@example.org", "userRightsGet");
246
-    }
247
-
248
-    public function testGetUserRightsWithoutAuthority() {
249
-        Phake::when(Arsse::$user)->authorize->thenReturn(false);
250
-        $this->assertException("notAuthorized", "User", "ExceptionAuthz");
251
-        Arsse::$db->userRightsGet("john.doe@example.com");
252
-    }
253
-
254
-    public function testSetUserRights() {
255
-        $user = "john.doe@example.com";
256
-        $rights = UserDriver::RIGHTS_GLOBAL_ADMIN;
257
-        $this->assertTrue(Arsse::$db->userRightsSet($user, $rights));
258
-        Phake::verify(Arsse::$user)->authorize($user, "userRightsSet", $rights);
259
-        $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','rights']]);
260
-        $state['arsse_users']['rows'][2][1] = $rights;
261
-        $this->compareExpectations($state);
262
-    }
263
-
264
-    public function testSetTheRightsOfAMissingUser() {
265
-        $rights = UserDriver::RIGHTS_GLOBAL_ADMIN;
266
-        $this->assertException("doesNotExist", "User");
267
-        Arsse::$db->userRightsSet("john.doe@example.org", $rights);
268
-    }
269
-
270
-    public function testSetUserRightsWithoutAuthority() {
271
-        $rights = UserDriver::RIGHTS_GLOBAL_ADMIN;
272
-        Phake::when(Arsse::$user)->authorize->thenReturn(false);
273
-        $this->assertException("notAuthorized", "User", "ExceptionAuthz");
274
-        Arsse::$db->userRightsSet("john.doe@example.com", $rights);
275
-    }
276 136
 }

+ 2
- 2
tests/lib/Result.php View File

@@ -37,11 +37,11 @@ class Result implements \JKingWeb\Arsse\Db\Result {
37 37
         return iterator_to_array($this, false);
38 38
     }
39 39
 
40
-    public function changes() {
40
+    public function changes(): int {
41 41
         return $this->rows;
42 42
     }
43 43
 
44
-    public function lastId() {
44
+    public function lastId(): int {
45 45
         return $this->id;
46 46
     }
47 47
 

+ 0
- 154
tests/lib/User/CommonTests.php View File

@@ -1,154 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\Test\User;
8
-
9
-use JKingWeb\Arsse\Arsse;
10
-use JKingWeb\Arsse\Conf;
11
-use JKingWeb\Arsse\User;
12
-use JKingWeb\Arsse\User\Driver;
13
-use Phake;
14
-
15
-trait CommonTests {
16
-    public function setUp() {
17
-        $this->clearData();
18
-        $conf = new Conf();
19
-        $conf->userDriver = $this->drv;
20
-        $conf->userPreAuth = false;
21
-        Arsse::$conf = $conf;
22
-        Arsse::$db = new Database();
23
-        Arsse::$user = Phake::partialMock(User::class);
24
-        Phake::when(Arsse::$user)->authorize->thenReturn(true);
25
-        $_SERVER['PHP_AUTH_USER'] = self::USER1;
26
-        $_SERVER['PHP_AUTH_PW'] = "secret";
27
-        // call the additional setup method if it exists
28
-        if (method_exists($this, "setUpSeries")) {
29
-            $this->setUpSeries();
30
-        }
31
-    }
32
-
33
-    public function tearDown() {
34
-        $this->clearData();
35
-        // call the additional teardiwn method if it exists
36
-        if (method_exists($this, "tearDownSeries")) {
37
-            $this->tearDownSeries();
38
-        }
39
-    }
40
-
41
-    public function testListUsers() {
42
-        $this->assertCount(0, Arsse::$user->list());
43
-    }
44
-
45
-    public function testCheckIfAUserDoesNotExist() {
46
-        $this->assertFalse(Arsse::$user->exists(self::USER1));
47
-    }
48
-
49
-    public function testAddAUser() {
50
-        Arsse::$user->add(self::USER1, "");
51
-        $this->assertCount(1, Arsse::$user->list());
52
-    }
53
-
54
-    public function testCheckIfAUserDoesExist() {
55
-        Arsse::$user->add(self::USER1, "");
56
-        $this->assertTrue(Arsse::$user->exists(self::USER1));
57
-    }
58
-
59
-    public function testAddADuplicateUser() {
60
-        Arsse::$user->add(self::USER1, "");
61
-        $this->assertException("alreadyExists", "User");
62
-        Arsse::$user->add(self::USER1, "");
63
-    }
64
-
65
-    public function testAddMultipleUsers() {
66
-        Arsse::$user->add(self::USER1, "");
67
-        Arsse::$user->add(self::USER2, "");
68
-        $this->assertCount(2, Arsse::$user->list());
69
-    }
70
-
71
-    public function testRemoveAUser() {
72
-        Arsse::$user->add(self::USER1, "");
73
-        $this->assertCount(1, Arsse::$user->list());
74
-        Arsse::$user->remove(self::USER1);
75
-        $this->assertCount(0, Arsse::$user->list());
76
-    }
77
-
78
-    public function testRemoveAMissingUser() {
79
-        $this->assertException("doesNotExist", "User");
80
-        Arsse::$user->remove(self::USER1);
81
-    }
82
-
83
-    /** @group slow */
84
-    public function testAuthenticateAUser() {
85
-        $_SERVER['PHP_AUTH_USER'] = self::USER1;
86
-        $_SERVER['PHP_AUTH_PW'] = "secret";
87
-        Arsse::$user->add(self::USER1, "secret");
88
-        Arsse::$user->add(self::USER2, "");
89
-        $this->assertTrue(Arsse::$user->auth());
90
-        $this->assertTrue(Arsse::$user->auth(self::USER1, "secret"));
91
-        $this->assertFalse(Arsse::$user->auth(self::USER1, "superman"));
92
-        $this->assertTrue(Arsse::$user->auth(self::USER2, ""));
93
-    }
94
-
95
-    /** @group slow */
96
-    public function testChangeAPassword() {
97
-        Arsse::$user->add(self::USER1, "secret");
98
-        $this->assertEquals("superman", Arsse::$user->passwordSet(self::USER1, "superman"));
99
-        $this->assertTrue(Arsse::$user->auth(self::USER1, "superman"));
100
-        $this->assertFalse(Arsse::$user->auth(self::USER1, "secret"));
101
-        $this->assertEquals("", Arsse::$user->passwordSet(self::USER1, ""));
102
-        $this->assertTrue(Arsse::$user->auth(self::USER1, ""));
103
-        $this->assertEquals(Arsse::$conf->userTempPasswordLength, strlen(Arsse::$user->passwordSet(self::USER1)));
104
-    }
105
-
106
-    public function testChangeAPasswordForAMissingUser() {
107
-        $this->assertException("doesNotExist", "User");
108
-        Arsse::$user->passwordSet(self::USER1, "superman");
109
-    }
110
-
111
-    public function testGetThePropertiesOfAUser() {
112
-        Arsse::$user->add(self::USER1, "secret");
113
-        $p = Arsse::$user->propertiesGet(self::USER1);
114
-        $this->assertArrayHasKey('id', $p);
115
-        $this->assertArrayHasKey('name', $p);
116
-        $this->assertArrayHasKey('domain', $p);
117
-        $this->assertArrayHasKey('rights', $p);
118
-        $this->assertArrayNotHasKey('password', $p);
119
-        $this->assertEquals(self::USER1, $p['name']);
120
-    }
121
-
122
-    public function testSetThePropertiesOfAUser() {
123
-        $pSet = [
124
-            'name'     => 'John Doe',
125
-            'id'       => 'invalid',
126
-            'domain'   => 'localhost',
127
-            'rights'   => Driver::RIGHTS_GLOBAL_ADMIN,
128
-            'password' => 'superman',
129
-        ];
130
-        $pGet = [
131
-            'name'   => 'John Doe',
132
-            'id'     => self::USER1,
133
-            'domain' => 'example.com',
134
-            'rights' => Driver::RIGHTS_NONE,
135
-        ];
136
-        Arsse::$user->add(self::USER1, "secret");
137
-        Arsse::$user->propertiesSet(self::USER1, $pSet);
138
-        $p = Arsse::$user->propertiesGet(self::USER1);
139
-        $this->assertArraySubset($pGet, $p);
140
-        $this->assertArrayNotHasKey('password', $p);
141
-        $this->assertFalse(Arsse::$user->auth(self::USER1, "superman"));
142
-    }
143
-
144
-    public function testGetTheRightsOfAUser() {
145
-        Arsse::$user->add(self::USER1, "");
146
-        $this->assertEquals(Driver::RIGHTS_NONE, Arsse::$user->rightsGet(self::USER1));
147
-    }
148
-
149
-    public function testSetTheRightsOfAUser() {
150
-        Arsse::$user->add(self::USER1, "");
151
-        Arsse::$user->rightsSet(self::USER1, Driver::RIGHTS_GLOBAL_ADMIN);
152
-        $this->assertEquals(Driver::RIGHTS_GLOBAL_ADMIN, Arsse::$user->rightsGet(self::USER1));
153
-    }
154
-}

+ 0
- 133
tests/lib/User/Database.php View File

@@ -1,133 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\Test\User;
8
-
9
-use JKingWeb\Arsse\Arsse;
10
-use JKingWeb\Arsse\User\Driver;
11
-use JKingWeb\Arsse\User\Exception;
12
-use JKingWeb\Arsse\User\ExceptionAuthz;
13
-use PasswordGenerator\Generator as PassGen;
14
-
15
-class Database extends DriverSkeleton {
16
-    public $db = [];
17
-
18
-    public function __construct() {
19
-    }
20
-
21
-    public function userExists(string $user): bool {
22
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
23
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
24
-        }
25
-        return parent::userExists($user);
26
-    }
27
-
28
-    public function userAdd(string $user, string $password = null): string {
29
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
30
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
31
-        }
32
-        if ($this->userExists($user)) {
33
-            throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
34
-        }
35
-        if ($password===null) {
36
-            $password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
37
-        }
38
-        return parent::userAdd($user, $password);
39
-    }
40
-
41
-    public function userRemove(string $user): bool {
42
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
43
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
44
-        }
45
-        if (!$this->userExists($user)) {
46
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
47
-        }
48
-        return parent::userRemove($user);
49
-    }
50
-
51
-    public function userList(string $domain = null): array {
52
-        if ($domain===null) {
53
-            if (!Arsse::$user->authorize("", __FUNCTION__)) {
54
-                throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
55
-            }
56
-            return parent::userList();
57
-        } else {
58
-            $suffix = '@'.$domain;
59
-            if (!Arsse::$user->authorize($suffix, __FUNCTION__)) {
60
-                throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
61
-            }
62
-            return parent::userList($domain);
63
-        }
64
-    }
65
-
66
-    public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
67
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
68
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
69
-        }
70
-        if (!$this->userExists($user)) {
71
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
72
-        }
73
-        if ($newPassword===null) {
74
-            $newPassword = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
75
-        }
76
-        return parent::userPasswordSet($user, $newPassword);
77
-    }
78
-
79
-    public function userPropertiesGet(string $user): array {
80
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
81
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
82
-        }
83
-        if (!$this->userExists($user)) {
84
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
85
-        }
86
-        $out = parent::userPropertiesGet($user);
87
-        unset($out['password']);
88
-        return $out;
89
-    }
90
-
91
-    public function userPropertiesSet(string $user, array $properties): array {
92
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
93
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
94
-        }
95
-        if (!$this->userExists($user)) {
96
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
97
-        }
98
-        parent::userPropertiesSet($user, $properties);
99
-        return $this->userPropertiesGet($user);
100
-    }
101
-
102
-    public function userRightsGet(string $user): int {
103
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
104
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
105
-        }
106
-        if (!$this->userExists($user)) {
107
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
108
-        }
109
-        return parent::userRightsGet($user);
110
-    }
111
-
112
-    public function userRightsSet(string $user, int $level): bool {
113
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
114
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
115
-        }
116
-        if (!$this->userExists($user)) {
117
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
118
-        }
119
-        return parent::userRightsSet($user, $level);
120
-    }
121
-
122
-    // specific to mock database
123
-
124
-    public function userPasswordGet(string $user): string {
125
-        if (!Arsse::$user->authorize($user, __FUNCTION__)) {
126
-            throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
127
-        }
128
-        if (!$this->userExists($user)) {
129
-            throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
130
-        }
131
-        return $this->db[$user]['password'];
132
-    }
133
-}

+ 0
- 127
tests/lib/User/DriverExternalMock.php View File

@@ -1,127 +0,0 @@
1
-<?php
2
-/** @license MIT
3
- * Copyright 2017 J. King, Dustin Wilson et al.
4
- * See LICENSE and AUTHORS files for details */
5
-
6
-declare(strict_types=1);
7
-namespace JKingWeb\Arsse\Test\User;
8
-
9
-use JKingWeb\Arsse\Arsse;
10
-use JKingWeb\Arsse\User\Driver;