Test Failed
Pull Request — master (#9)
by Maximo
03:26
created

library/Acl/Manager.php (1 issue)

1
<?php
0 ignored issues
show
End of line character is invalid; expected "\n" but found "\r\n"
Loading history...
2
declare(strict_types=1);
3
4
namespace Gewaer\Acl;
5
6
use Phalcon\Db;
7
use Phalcon\Db\AdapterInterface as DbAdapter;
8
use Phalcon\Acl\Exception;
9
use Phalcon\Acl\Resource;
10
use Phalcon\Acl;
11
use Phalcon\Acl\Role;
12
use Phalcon\Acl\RoleInterface;
13
use Gewaer\Models\Companies;
14
use Gewaer\Models\Apps;
15
use Phalcon\Acl\Adapter\Database as PhalconAclDatabaseAdapter;
16
use Phalcon\Acl\Adapter;
17
use BadMethodCallException;
18
19
/**
20
 * Phalcon\Acl\Adapter\Database
21
 * Manages Geweaer Multi tenant ACL lists in database
22
 *
23
 * #extends PhalconAclDatabaseAdapter #had to comments it out testing breaking
24
 */
25
class Manager extends Adapter
26
{
27
    /**
28
     * @var DbAdapter
29
     */
30
    protected $connection;
31
32
    /**
33
     * Roles table
34
     * @var string
35
     */
36
    protected $roles;
37
38
    /**
39
     * Resources table
40
     * @var string
41
     */
42
    protected $resources;
43
44
    /**
45
     * Resources Accesses table
46
     * @var string
47
     */
48
    protected $resourcesAccesses;
49
50
    /**
51
     * Access List table
52
     * @var string
53
     */
54
    protected $accessList;
55
56
    /**
57
     * Roles Inherits table
58
     * @var string
59
     */
60
    protected $rolesInherits;
61
62
    /**
63
     * Default action for no arguments is allow
64
     * @var int
65
     */
66
    protected $noArgumentsDefaultAction = Acl::ALLOW;
67
68
    /**
69
     * Company Object
70
     *
71
     * @var Companies
72
     */
73
    protected $company;
74
75
    /**
76
     * App Objc
77
     *
78
     * @var Apps
79
     */
80
    protected $app;
81
82
    /**
83
     * Class constructor.
84
     *
85
     * @param  array $options Adapter config
86
     * @throws Exception
87
     */
88 12
    public function __construct(array $options)
89
    {
90 12
        if (!isset($options['db']) || !$options['db'] instanceof DbAdapter) {
91
            throw new Exception(
92
                'Parameter "db" is required and it must be an instance of Phalcon\Acl\AdapterInterface'
93
            );
94
        }
95
96 12
        $this->connection = $options['db'];
97
98 12
        foreach (['roles', 'resources', 'resourcesAccesses', 'accessList', 'rolesInherits'] as $table) {
99 12
            if (!isset($options[$table]) || empty($options[$table]) || !is_string($options[$table])) {
100
                throw new Exception("Parameter '{$table}' is required and it must be a non empty string");
101
            }
102
103 12
            $this->{$table} = $this->connection->escapeIdentifier($options[$table]);
104
        }
105 12
    }
106
107
    /**
108
     * Set current user Company
109
     *
110
     * @param Companies $company
111
     * @return void
112
     */
113
    public function setCompany(Companies $company): void
114
    {
115
        $this->company = $company;
116
    }
117
118
    /**
119
     * Set current user app
120
     *
121
     * @param Apps $app
122
     * @return void
123
     */
124 6
    public function setApp(Apps $app): void
125
    {
126 6
        $this->app = $app;
127 6
    }
128
129
    /**
130
     * Get the current App
131
     *
132
     * @return void
133
     */
134 5
    public function getApp(): Apps
135
    {
136 5
        if (!is_object($this->app)) {
137
            $this->app = new Apps();
138
            $this->app->id = 0;
139
            $this->app->name = 'Canvas';
140
        }
141
142 5
        return $this->app;
143
    }
144
145
    /**
146
     * Get the current App
147
     *
148
     * @return void
149
     */
150
    public function getCompany() : Companies
151
    {
152
        if (!is_object($this->company)) {
153
            $this->company = new Companies();
154
            $this->company->id = 0;
155
            $this->company->name = 'Canvas';
156
        }
157
158
        return $this->company;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     *
164
     * Example:
165
     * <code>
166
     * $acl->addRole(new Phalcon\Acl\Role('administrator'), 'consultor');
167
     * $acl->addRole('administrator', 'consultor');
168
     * </code>
169
     *
170
     * @param  \Phalcon\Acl\Role|string $role
171
     * @param  int   $scope
172
     * @param  string                   $accessInherits
173
     * @return boolean
174
     * @throws \Phalcon\Acl\Exception
175
     */
176 2
    public function addRole($role, $scope = 0, $accessInherits = null): bool
177
    {
178 2
        if (is_string($role)) {
179 1
            $role = $this->setAppByRole($role);
180
181 1
            $role = new Role($role, ucwords($role) . ' Role');
182
        }
183 2
        if (!$role instanceof RoleInterface) {
184
            throw new Exception('Role must be either an string or implement RoleInterface');
185
        }
186
187 2
        $exists = $this->connection->fetchOne(
188 2
            "SELECT COUNT(*) FROM {$this->roles} WHERE name = ?",
189 2
            null,
190 2
            [$role->getName()]
191
        );
192
193 2
        if (!$exists[0]) {
194
            $this->connection->execute(
195
                "INSERT INTO {$this->roles} (name, description, company_id, apps_id, scope, created_at) VALUES (?, ?, ?, ?, ?, ?)",
196
                [$role->getName(), $role->getDescription(), $this->getCompany()->getId(), $this->getApp()->getId(), $scope, date('Y-m-d H:i:s')]
197
            );
198
            $this->connection->execute(
199
                "INSERT INTO {$this->accessList} (roles_name, resources_name, access_name, allowed, apps_id, created_at) VALUES (?, ?, ?, ?, ?, ?)",
200
                [$role->getName(), '*', '*', $this->_defaultAccess, $this->getApp()->getId(), date('Y-m-d H:i:s')]
201
            );
202
        }
203 2
        if ($accessInherits) {
204
            return $this->addInherit($role->getName(), $accessInherits);
205
        }
206 2
        return true;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     *
212
     * @param  string $roleName
213
     * @param  string $roleToInherit
214
     * @throws \Phalcon\Acl\Exception
215
     */
216
    public function addInherit($roleName, $roleToInherit): bool
217
    {
218
        $sql = "SELECT COUNT(*) FROM {$this->roles} WHERE name = ?";
219
        $exists = $this->connection->fetchOne($sql, null, [$roleName]);
220
        if (!$exists[0]) {
221
            throw new Exception("Role '{$roleName}' does not exist in the role list");
222
        }
223
        $exists = $this->connection->fetchOne(
224
            "SELECT COUNT(*) FROM {$this->rolesInherits} WHERE roles_name = ? AND roles_inherit = ?",
225
            null,
226
            [$roleName, $roleToInherit]
227
        );
228
        if (!$exists[0]) {
229
            $this->connection->execute(
230
                "INSERT INTO {$this->rolesInherits} VALUES (?, ?)",
231
                [$roleName, $roleToInherit]
232
            );
233
        }
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     *
239
     * @param  string  $roleName
240
     * @return boolean
241
     */
242 2
    public function isRole($roleName): bool
243
    {
244 2
        $exists = $this->connection->fetchOne(
245 2
            "SELECT COUNT(*) FROM {$this->roles} WHERE name = ?",
246 2
            null,
247 2
            [$roleName]
248
        );
249 2
        return (bool) $exists[0];
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     *
255
     * @param  string  $resourceName
256
     * @return boolean
257
     */
258 1
    public function isResource($resourceName): bool
259
    {
260 1
        $exists = $this->connection->fetchOne(
261 1
            "SELECT COUNT(*) FROM {$this->resources} WHERE name = ?",
262 1
            null,
263 1
            [$resourceName]
264
        );
265 1
        return (bool) $exists[0];
266
    }
267
268
    /**
269
     * Given a resource with a dot CRM.Leads , it will set the app
270
     *
271
     * @param string $resource
272
     * @return void
273
     */
274 5
    protected function setAppByResource(string $resource): string
275
    {
276
        //echeck if we have a dot , taht means we are sending the specific app to use
277 5
        if (strpos($resource, '.') !== false) {
278 5
            $appResource = explode('.', $resource);
279 5
            $resource = $appResource[1];
280 5
            $appName = $appResource[0];
281
282
            //look for the app and set it
283 5
            if ($app = Apps::getACLApp($appName)) {
284 5
                $this->setApp($app);
285
            }
286
        }
287
288 5
        return $resource;
289
    }
290
291
    /**
292
     * Given a resource with a dot CRM.Leads , it will set the app
293
     *
294
     * @param string $resource
295
     * @return void
296
     */
297 3
    protected function setAppByRole(string $role) : string
298
    {
299
        //echeck if we have a dot , taht means we are sending the specific app to use
300 3
        if (strpos($role, '.') !== false) {
301 1
            $appRole = explode('.', $role);
302 1
            $role = $appRole[1];
303 1
            $appName = $appRole[0];
304
305
            //look for the app and set it
306 1
            if ($app = Apps::getACLApp($appName)) {
307 1
                $this->setApp($app);
308
            }
309
        }
310
311 3
        return $role;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     * Example:
317
     * <code>
318
     * //Add a resource to the the list allowing access to an action
319
     * $acl->addResource(new Phalcon\Acl\Resource('customers'), 'search');
320
     * $acl->addResource('customers', 'search');
321
     * //Add a resource  with an access list
322
     * $acl->addResource(new Phalcon\Acl\Resource('customers'), ['create', 'search']);
323
     * $acl->addResource('customers', ['create', 'search']);
324
     * $acl->addResource('App.customers', ['create', 'search']);
325
     * </code>
326
     *
327
     * @param  \Phalcon\Acl\Resource|string $resource
328
     * @param  array|string                 $accessList
329
     * @return boolean
330
     */
331 1
    public function addResource($resource, $accessList = null): bool
332
    {
333 1
        if (!is_object($resource)) {
334
            //echeck if we have a dot , taht means we are sending the specific app to use
335 1
            $resource = $this->setAppByResource($resource);
336
337 1
            $resource = new Resource($resource);
338
        }
339
340 1
        $exists = $this->connection->fetchOne(
341 1
            "SELECT COUNT(*) FROM {$this->resources} WHERE name = ?",
342 1
            null,
343 1
            [$resource->getName()]
344
        );
345
346 1
        if (!$exists[0]) {
347 1
            $this->connection->execute(
348 1
                "INSERT INTO {$this->resources} (name, description, apps_id, created_at) VALUES (?, ?, ?, ?)",
349 1
                [$resource->getName(), $resource->getDescription(), $this->getApp()->getId(), date('Y-m-d H:i:s')]
350
            );
351
        }
352
353 1
        if ($accessList) {
354 1
            return $this->addResourceAccess($resource->getName(), $accessList);
355
        }
356
357
        return true;
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     *
363
     * @param  string       $resourceName
364
     * @param  array|string $accessList
365
     * @return boolean
366
     * @throws \Phalcon\Acl\Exception
367
     */
368 1
    public function addResourceAccess($resourceName, $accessList): bool
369
    {
370 1
        if (!$this->isResource($resourceName)) {
371
            throw new Exception("Resource '{$resourceName}' does not exist in ACL");
372
        }
373
374 1
        $sql = "SELECT COUNT(*) FROM {$this->resourcesAccesses} WHERE resources_name = ? AND access_name = ? AND apps_id = ?";
375 1
        if (!is_array($accessList)) {
376
            $accessList = [$accessList];
377
        }
378
379 1
        foreach ($accessList as $accessName) {
380 1
            $exists = $this->connection->fetchOne($sql, null, [$resourceName, $accessName, $this->getApp()->getId()]);
381 1
            if (!$exists[0]) {
382 1
                $this->connection->execute(
383 1
                    'INSERT INTO ' . $this->resourcesAccesses . ' (resources_name, access_name, apps_id, created_at) VALUES (?, ?, ?, ?)',
384 1
                    [$resourceName, $accessName, $this->getApp()->getId(), date('Y-m-d H:i:s')]
385
                );
386
            }
387
        }
388 1
        return true;
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     *
394
     * @return \Phalcon\Acl\Resource[]
395
     */
396
    public function getResources(): \Phalcon\Acl\ResourceInterface
397
    {
398
        $resources = [];
399
        $sql = "SELECT * FROM {$this->resources}";
400
        foreach ($this->connection->fetchAll($sql, Db::FETCH_ASSOC) as $row) {
401
            $resources[] = new Resource($row['name'], $row['description']);
402
        }
403
        return $resources;
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     *
409
     * @return RoleInterface[]
410
     */
411
    public function getRoles(): \Phalcon\Acl\RoleInterface
412
    {
413
        $roles = [];
414
        $sql = "SELECT * FROM {$this->roles}";
415
        foreach ($this->connection->fetchAll($sql, Db::FETCH_ASSOC) as $row) {
416
            $roles[] = new Role($row['name'], $row['description']);
417
        }
418
        return $roles;
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     *
424
     * @param string       $resourceName
425
     * @param array|string $accessList
426
     */
427
    public function dropResourceAccess($resourceName, $accessList)
428
    {
429
        throw new BadMethodCallException('Not implemented yet.');
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     * You can use '*' as wildcard
435
     * Example:
436
     * <code>
437
     * //Allow access to guests to search on customers
438
     * $acl->allow('guests', 'customers', 'search');
439
     * //Allow access to guests to search or create on customers
440
     * $acl->allow('guests', 'customers', ['search', 'create']);
441
     * //Allow access to any role to browse on products
442
     * $acl->allow('*', 'products', 'browse');
443
     * //Allow access to any role to browse on any resource
444
     * $acl->allow('*', '*', 'browse');
445
     * </code>
446
     *
447
     * @param string       $roleName
448
     * @param string       $resourceName
449
     * @param array|string $access
450
     * @param mixed $func
451
     */
452 1
    public function allow($roleName, $resourceName, $access, $func = null)
453
    {
454 1
        return $this->allowOrDeny($roleName, $resourceName, $access, Acl::ALLOW);
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     * You can use '*' as wildcard
460
     * Example:
461
     * <code>
462
     * //Deny access to guests to search on customers
463
     * $acl->deny('guests', 'customers', 'search');
464
     * //Deny access to guests to search or create on customers
465
     * $acl->deny('guests', 'customers', ['search', 'create']);
466
     * //Deny access to any role to browse on products
467
     * $acl->deny('*', 'products', 'browse');
468
     * //Deny access to any role to browse on any resource
469
     * $acl->deny('*', '*', 'browse');
470
     * </code>
471
     *
472
     * @param  string       $roleName
473
     * @param  string       $resourceName
474
     * @param  array|string $access
475
     * @param  mixed $func
476
     * @return boolean
477
     */
478 1
    public function deny($roleName, $resourceName, $access, $func = null)
479
    {
480 1
        return $this->allowOrDeny($roleName, $resourceName, $access, Acl::DENY);
481
    }
482
483
    /**
484
     * {@inheritdoc}
485
     * Example:
486
     * <code>
487
     * //Does Andres have access to the customers resource to create?
488
     * $acl->isAllowed('Andres', 'Products', 'create');
489
     * //Do guests have access to any resource to edit?
490
     * $acl->isAllowed('guests', '*', 'edit');
491
     * </code>
492
     *
493
     * @param string $role
494
     * @param string $resource
495
     * @param string $access
496
     * @param array  $parameters
497
     * @return bool
498
     */
499 2
    public function isAllowed($role, $resource, $access, array $parameters = null): bool
500
    {
501 2
        $role = $this->setAppByRole($role);
502
        //resoure always overwrites the role app?
503 2
        $resource = $this->setAppByResource($resource);
504
505 2
        $sql = implode(' ', [
506 2
            'SELECT ' . $this->connection->escapeIdentifier('allowed') . " FROM {$this->accessList} AS a",
507
            // role_name in:
508 2
            'WHERE roles_name IN (',
509
                // given 'role'-parameter
510 2
            'SELECT ? ',
511
                // inherited role_names
512 2
            "UNION SELECT roles_inherit FROM {$this->rolesInherits} WHERE roles_name = ?",
513
                // or 'any'
514 2
            "UNION SELECT '*'",
515 2
            ')',
516
            // resources_name should be given one or 'any'
517 2
            "AND resources_name IN (?, '*')",
518
            // access_name should be given one or 'any'
519
            //"AND access_name IN (?, '*')", you need to specify * , we are forcing to check always for permisions
520 2
            'AND access_name IN (?)',
521 2
            'AND apps_id = ? ',
522
            // order be the sum of bools for 'literals' before 'any'
523 2
            'ORDER BY ' . $this->connection->escapeIdentifier('allowed') . ' DESC',
524
            // get only one...
525 2
            'LIMIT 1'
526
        ]);
527
528
        // fetch one entry...
529 2
        $allowed = $this->connection->fetchOne($sql, Db::FETCH_NUM, [$role, $role, $resource, $access, $this->getApp()->getId()]);
530
531 2
        if (is_array($allowed)) {
532 2
            return (bool) $allowed[0];
533
        }
534
535
        /**
536
         * Return the default access action
537
         */
538
        return (bool) $this->_defaultAccess;
539
    }
540
541
    /**
542
     * Returns the default ACL access level for no arguments provided
543
     * in isAllowed action if there exists func for accessKey
544
     *
545
     * @return int
546
     */
547
    public function getNoArgumentsDefaultAction(): int
548
    {
549
        return $this->noArgumentsDefaultAction;
550
    }
551
552
    /**
553
     * Sets the default access level for no arguments provided
554
     * in isAllowed action if there exists func for accessKey
555
     *
556
     * @param int $defaultAccess Phalcon\Acl::ALLOW or Phalcon\Acl::DENY
557
     */
558
    public function setNoArgumentsDefaultAction($defaultAccess)
559
    {
560
        $this->noArgumentsDefaultAction = intval($defaultAccess);
561
    }
562
563
    /**
564
     * Inserts/Updates a permission in the access list
565
     *
566
     * @param  string  $roleName
567
     * @param  string  $resourceName
568
     * @param  string  $accessName
569
     * @param  integer $action
570
     * @return boolean
571
     * @throws \Phalcon\Acl\Exception
572
     */
573 2
    protected function insertOrUpdateAccess($roleName, $resourceName, $accessName, $action)
574
    {
575 2
        $resourceName = $this->setAppByResource($resourceName);
576
577
        /**
578
         * Check if the access is valid in the resource unless wildcard
579
         */
580 2
        if ($resourceName !== '*' && $accessName !== '*') {
581 2
            $sql = "SELECT COUNT(*) FROM {$this->resourcesAccesses} WHERE resources_name = ? AND access_name = ? and apps_id  = ?";
582 2
            $exists = $this->connection->fetchOne($sql, null, [$resourceName, $accessName, $this->getApp()->getId()]);
583 2
            if (!$exists[0]) {
584
                throw new Exception(
585
                    "Access '{$accessName}' does not exist in resource '{$resourceName}' in ACL"
586
                );
587
            }
588
        }
589
        /**
590
         * Update the access in access_list
591
         */
592 2
        $sql = "SELECT COUNT(*) FROM {$this->accessList} "
593 2
            . ' WHERE roles_name = ? AND resources_name = ? AND access_name = ? AND apps_id = ?';
594 2
        $exists = $this->connection->fetchOne($sql, null, [$roleName, $resourceName, $accessName, $this->getApp()->getId()]);
595 2
        if (!$exists[0]) {
596 2
            $sql = "INSERT INTO {$this->accessList} (roles_name, resources_name, access_name, allowed, apps_id, created_at) VALUES (?, ?, ?, ?, ?, ?)";
597 2
            $params = [$roleName, $resourceName, $accessName, $action, $this->getApp()->getId(), date('Y-m-d H:i:s')];
598
        } else {
599
            $sql = "UPDATE {$this->accessList} SET allowed = ? " .
600
                'WHERE roles_name = ? AND resources_name = ? AND access_name = ? AND apps_id = ?';
601
            $params = [$action, $roleName, $resourceName, $accessName, $this->getApp()->getId()];
602
        }
603 2
        $this->connection->execute($sql, $params);
604
605
        /**
606
         * Update the access '*' in access_list
607
         */
608 2
        $sql = "SELECT COUNT(*) FROM {$this->accessList} " .
609 2
            'WHERE roles_name = ? AND resources_name = ? AND access_name = ? and apps_id = ?';
610 2
        $exists = $this->connection->fetchOne($sql, null, [$roleName, $resourceName, '*', $this->getApp()->getId()]);
611 2
        if (!$exists[0]) {
612 1
            $sql = "INSERT INTO {$this->accessList} (roles_name, resources_name, access_name, allowed, apps_id, created_at) VALUES (?, ?, ?, ?, ? , ?)";
613 1
            $this->connection->execute($sql, [$roleName, $resourceName, '*', $this->_defaultAccess, $this->getApp()->getId(), date('Y-m-d H:i:s')]);
614
        }
615
616 2
        return true;
617
    }
618
619
    /**
620
     * Inserts/Updates a permission in the access list
621
     *
622
     * @param  string       $roleName
623
     * @param  string       $resourceName
624
     * @param  array|string $access
625
     * @param  integer      $action
626
     * @throws \Phalcon\Acl\Exception
627
     */
628 2
    protected function allowOrDeny($roleName, $resourceName, $access, $action)
629
    {
630 2
        if (!$this->isRole($roleName)) {
631
            throw new Exception("Role '{$roleName}' does not exist in the list");
632
        }
633 2
        if (!is_array($access)) {
634
            $access = [$access];
635
        }
636 2
        foreach ($access as $accessName) {
637 2
            $this->insertOrUpdateAccess($roleName, $resourceName, $accessName, $action);
638
        }
639
640 2
        return true;
641
    }
642
}
643