summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/services/access_control.py
blob: 27d849cc4456739a0668533f20d216c67c3a14a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
# -*- coding: utf-8 -*-
# pylint: disable=too-many-arguments,too-many-return-statements
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
from __future__ import absolute_import

import errno
import json
import logging
import re
import threading
import time
from datetime import datetime, timedelta
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
from typing import List, Optional, Sequence

import bcrypt
from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand

from .. import mgr
from ..exceptions import PasswordPolicyException, PermissionNotValid, \
    PwdExpirationDateNotValid, RoleAlreadyExists, RoleDoesNotExist, \
    RoleIsAssociatedWithUser, RoleNotInUser, ScopeNotInRole, ScopeNotValid, \
    UserAlreadyExists, UserDoesNotExist
from ..security import Permission, Scope
from ..settings import Settings

logger = logging.getLogger('access_control')
DEFAULT_FILE_DESC = 'password/secret'


# password hashing algorithm
def password_hash(password, salt_password=None):
    if not password:
        return None
    if not salt_password:
        salt_password = bcrypt.gensalt()
    else:
        salt_password = salt_password.encode('utf8')
    return bcrypt.hashpw(password.encode('utf8'), salt_password).decode('utf8')


_P = Permission  # short alias


class PasswordPolicy(object):
    def __init__(self, password, username=None, old_password=None):
        """
        :param password: The new plain password.
        :type password: str
        :param username: The name of the user.
        :type username: str | None
        :param old_password: The old plain password.
        :type old_password: str | None
        """
        self.password = password
        self.username = username
        self.old_password = old_password
        self.forbidden_words = Settings.PWD_POLICY_EXCLUSION_LIST.split(',')
        self.complexity_credits = 0

    @staticmethod
    def _check_if_contains_word(password, word):
        return re.compile('(?:{0})'.format(word),
                          flags=re.IGNORECASE).search(password)

    def check_password_complexity(self):
        if not Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED:
            return Settings.PWD_POLICY_MIN_COMPLEXITY
        digit_credit = 1
        small_letter_credit = 1
        big_letter_credit = 2
        special_character_credit = 3
        other_character_credit = 5
        self.complexity_credits = 0
        for ch in self.password:
            if ch in ascii_uppercase:
                self.complexity_credits += big_letter_credit
            elif ch in ascii_lowercase:
                self.complexity_credits += small_letter_credit
            elif ch in digits:
                self.complexity_credits += digit_credit
            elif ch in punctuation:
                self.complexity_credits += special_character_credit
            else:
                self.complexity_credits += other_character_credit
        return self.complexity_credits

    def check_is_old_password(self):
        if not Settings.PWD_POLICY_CHECK_OLDPWD_ENABLED:
            return False
        return self.old_password and self.password == self.old_password

    def check_if_contains_username(self):
        if not Settings.PWD_POLICY_CHECK_USERNAME_ENABLED:
            return False
        if not self.username:
            return False
        return self._check_if_contains_word(self.password, self.username)

    def check_if_contains_forbidden_words(self):
        if not Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED:
            return False
        return self._check_if_contains_word(self.password,
                                            '|'.join(self.forbidden_words))

    def check_if_sequential_characters(self):
        if not Settings.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED:
            return False
        for i in range(1, len(self.password) - 1):
            if ord(self.password[i - 1]) + 1 == ord(self.password[i])\
               == ord(self.password[i + 1]) - 1:
                return True
        return False

    def check_if_repetitive_characters(self):
        if not Settings.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED:
            return False
        for i in range(1, len(self.password) - 1):
            if self.password[i - 1] == self.password[i] == self.password[i + 1]:
                return True
        return False

    def check_password_length(self):
        if not Settings.PWD_POLICY_CHECK_LENGTH_ENABLED:
            return True
        return len(self.password) >= Settings.PWD_POLICY_MIN_LENGTH

    def check_all(self):
        """
        Perform all password policy checks.
        :raise PasswordPolicyException: If a password policy check fails.
        """
        if not Settings.PWD_POLICY_ENABLED:
            return
        if self.check_password_complexity() < Settings.PWD_POLICY_MIN_COMPLEXITY:
            raise PasswordPolicyException('Password is too weak.')
        if not self.check_password_length():
            raise PasswordPolicyException('Password is too weak.')
        if self.check_is_old_password():
            raise PasswordPolicyException('Password must not be the same as the previous one.')
        if self.check_if_contains_username():
            raise PasswordPolicyException('Password must not contain username.')
        result = self.check_if_contains_forbidden_words()
        if result:
            raise PasswordPolicyException('Password must not contain the keyword "{}".'.format(
                result.group(0)))
        if self.check_if_repetitive_characters():
            raise PasswordPolicyException('Password must not contain repetitive characters.')
        if self.check_if_sequential_characters():
            raise PasswordPolicyException('Password must not contain sequential characters.')


class Role(object):
    def __init__(self, name, description=None, scope_permissions=None):
        self.name = name
        self.description = description
        if scope_permissions is None:
            self.scopes_permissions = {}
        else:
            self.scopes_permissions = scope_permissions

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return self.name == other.name

    def set_scope_permissions(self, scope, permissions):
        if not Scope.valid_scope(scope):
            raise ScopeNotValid(scope)
        for perm in permissions:
            if not Permission.valid_permission(perm):
                raise PermissionNotValid(perm)

        permissions.sort()
        self.scopes_permissions[scope] = permissions

    def del_scope_permissions(self, scope):
        if scope not in self.scopes_permissions:
            raise ScopeNotInRole(scope, self.name)
        del self.scopes_permissions[scope]

    def reset_scope_permissions(self):
        self.scopes_permissions = {}

    def authorize(self, scope, permissions):
        if scope in self.scopes_permissions:
            role_perms = self.scopes_permissions[scope]
            for perm in permissions:
                if perm not in role_perms:
                    return False
            return True
        return False

    def to_dict(self):
        return {
            'name': self.name,
            'description': self.description,
            'scopes_permissions': self.scopes_permissions
        }

    @classmethod
    def from_dict(cls, r_dict):
        return Role(r_dict['name'], r_dict['description'],
                    r_dict['scopes_permissions'])


# static pre-defined system roles
# this roles cannot be deleted nor updated

# admin role provides all permissions for all scopes
ADMIN_ROLE = Role(
    'administrator', 'allows full permissions for all security scopes', {
        scope_name: Permission.all_permissions()
        for scope_name in Scope.all_scopes()
    })


# read-only role provides read-only permission for all scopes
READ_ONLY_ROLE = Role(
    'read-only',
    'allows read permission for all security scope except dashboard settings and config-opt', {
        scope_name: [_P.READ] for scope_name in Scope.all_scopes()
        if scope_name not in (Scope.DASHBOARD_SETTINGS, Scope.CONFIG_OPT)
    })


# block manager role provides all permission for block related scopes
BLOCK_MGR_ROLE = Role(
    'block-manager', 'allows full permissions for rbd-image, rbd-mirroring, and iscsi scopes', {
        Scope.RBD_IMAGE: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.POOL: [_P.READ],
        Scope.ISCSI: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.RBD_MIRRORING: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.GRAFANA: [_P.READ],
    })


# RadosGW manager role provides all permissions for block related scopes
RGW_MGR_ROLE = Role(
    'rgw-manager', 'allows full permissions for the rgw scope', {
        Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.GRAFANA: [_P.READ],
    })


# Cluster manager role provides all permission for OSDs, Monitors, and
# Config options
CLUSTER_MGR_ROLE = Role(
    'cluster-manager', """allows full permissions for the hosts, osd, mon, mgr,
    and config-opt scopes""", {
        Scope.HOSTS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.OSD: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.MONITOR: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.MANAGER: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.CONFIG_OPT: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.LOG: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.GRAFANA: [_P.READ],
    })


# Pool manager role provides all permissions for pool related scopes
POOL_MGR_ROLE = Role(
    'pool-manager', 'allows full permissions for the pool scope', {
        Scope.POOL: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.GRAFANA: [_P.READ],
    })

# CephFS manager role provides all permissions for CephFS related scopes
CEPHFS_MGR_ROLE = Role(
    'cephfs-manager', 'allows full permissions for the cephfs scope', {
        Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.GRAFANA: [_P.READ],
    })

GANESHA_MGR_ROLE = Role(
    'ganesha-manager', 'allows full permissions for the nfs-ganesha scope', {
        Scope.NFS_GANESHA: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
        Scope.GRAFANA: [_P.READ],
    })


SYSTEM_ROLES = {
    ADMIN_ROLE.name: ADMIN_ROLE,
    READ_ONLY_ROLE.name: READ_ONLY_ROLE,
    BLOCK_MGR_ROLE.name: BLOCK_MGR_ROLE,
    RGW_MGR_ROLE.name: RGW_MGR_ROLE,
    CLUSTER_MGR_ROLE.name: CLUSTER_MGR_ROLE,
    POOL_MGR_ROLE.name: POOL_MGR_ROLE,
    CEPHFS_MGR_ROLE.name: CEPHFS_MGR_ROLE,
    GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE,
}


class User(object):
    def __init__(self, username, password, name=None, email=None, roles=None,
                 last_update=None, enabled=True, pwd_expiration_date=None,
                 pwd_update_required=False):
        self.username = username
        self.password = password
        self.name = name
        self.email = email
        self.invalid_auth_attempt = 0
        if roles is None:
            self.roles = set()
        else:
            self.roles = roles
        if last_update is None:
            self.refresh_last_update()
        else:
            self.last_update = last_update
        self._enabled = enabled
        self.pwd_expiration_date = pwd_expiration_date
        if self.pwd_expiration_date is None:
            self.refresh_pwd_expiration_date()
        self.pwd_update_required = pwd_update_required

    def refresh_last_update(self):
        self.last_update = int(time.time())

    def refresh_pwd_expiration_date(self):
        if Settings.USER_PWD_EXPIRATION_SPAN > 0:
            expiration_date = datetime.utcnow() + timedelta(
                days=Settings.USER_PWD_EXPIRATION_SPAN)
            self.pwd_expiration_date = int(time.mktime(expiration_date.timetuple()))
        else:
            self.pwd_expiration_date = None

    @property
    def enabled(self):
        return self._enabled

    @enabled.setter
    def enabled(self, value):
        self._enabled = value
        self.refresh_last_update()

    def set_password(self, password):
        self.set_password_hash(password_hash(password))

    def set_password_hash(self, hashed_password):
        self.invalid_auth_attempt = 0
        self.password = hashed_password
        self.refresh_last_update()
        self.refresh_pwd_expiration_date()
        self.pwd_update_required = False

    def compare_password(self, password):
        """
        Compare the specified password with the user password.
        :param password: The plain password to check.
        :type password: str
        :return: `True` if the passwords are equal, otherwise `False`.
        :rtype: bool
        """
        pass_hash = password_hash(password, salt_password=self.password)
        return pass_hash == self.password

    def is_pwd_expired(self):
        if self.pwd_expiration_date:
            current_time = int(time.mktime(datetime.utcnow().timetuple()))
            return self.pwd_expiration_date < current_time
        return False

    def set_roles(self, roles):
        self.roles = set(roles)
        self.refresh_last_update()

    def add_roles(self, roles):
        self.roles = self.roles.union(set(roles))
        self.refresh_last_update()

    def del_roles(self, roles):
        for role in roles:
            if role not in self.roles:
                raise RoleNotInUser(role.name, self.username)
        self.roles.difference_update(set(roles))
        self.refresh_last_update()

    def authorize(self, scope, permissions):
        if self.pwd_update_required:
            return False

        for role in self.roles:
            if role.authorize(scope, permissions):
                return True
        return False

    def permissions_dict(self):
        # type: () -> dict
        perms = {}  # type: dict
        for role in self.roles:
            for scope, perms_list in role.scopes_permissions.items():
                if scope in perms:
                    perms_tmp = set(perms[scope]).union(set(perms_list))
                    perms[scope] = list(perms_tmp)
                else:
                    perms[scope] = perms_list

        return perms

    def to_dict(self):
        return {
            'username': self.username,
            'password': self.password,
            'roles': sorted([r.name for r in self.roles]),
            'name': self.name,
            'email': self.email,
            'lastUpdate': self.last_update,
            'enabled': self.enabled,
            'pwdExpirationDate': self.pwd_expiration_date,
            'pwdUpdateRequired': self.pwd_update_required
        }

    @classmethod
    def from_dict(cls, u_dict, roles):
        return User(u_dict['username'], u_dict['password'], u_dict['name'],
                    u_dict['email'], {roles[r] for r in u_dict['roles']},
                    u_dict['lastUpdate'], u_dict['enabled'],
                    u_dict['pwdExpirationDate'], u_dict['pwdUpdateRequired'])


class AccessControlDB(object):
    VERSION = 2
    ACDB_CONFIG_KEY = "accessdb_v"

    def __init__(self, version, users, roles):
        self.users = users
        self.version = version
        self.roles = roles
        self.lock = threading.RLock()

    def create_role(self, name, description=None):
        with self.lock:
            if name in SYSTEM_ROLES or name in self.roles:
                raise RoleAlreadyExists(name)
            role = Role(name, description)
            self.roles[name] = role
            return role

    def get_role(self, name):
        with self.lock:
            if name not in self.roles:
                raise RoleDoesNotExist(name)
            return self.roles[name]

    def increment_attempt(self, username):
        with self.lock:
            if username in self.users:
                self.users[username].invalid_auth_attempt += 1

    def reset_attempt(self, username):
        with self.lock:
            if username in self.users:
                self.users[username].invalid_auth_attempt = 0

    def get_attempt(self, username):
        with self.lock:
            try:
                return self.users[username].invalid_auth_attempt
            except KeyError:
                return 0

    def delete_role(self, name):
        with self.lock:
            if name not in self.roles:
                raise RoleDoesNotExist(name)
            role = self.roles[name]

            # check if role is not associated with a user
            for username, user in self.users.items():
                if role in user.roles:
                    raise RoleIsAssociatedWithUser(name, username)

            del self.roles[name]

    def create_user(self, username, password, name, email, enabled=True,
                    pwd_expiration_date=None, pwd_update_required=False):
        logger.debug("creating user: username=%s", username)
        with self.lock:
            if username in self.users:
                raise UserAlreadyExists(username)
            if pwd_expiration_date and \
               (pwd_expiration_date < int(time.mktime(datetime.utcnow().timetuple()))):
                raise PwdExpirationDateNotValid()
            user = User(username, password_hash(password), name, email, enabled=enabled,
                        pwd_expiration_date=pwd_expiration_date,
                        pwd_update_required=pwd_update_required)
            self.users[username] = user
            return user

    def get_user(self, username):
        with self.lock:
            if username not in self.users:
                raise UserDoesNotExist(username)
            return self.users[username]

    def delete_user(self, username):
        with self.lock:
            if username not in self.users:
                raise UserDoesNotExist(username)
            del self.users[username]

    def update_users_with_roles(self, role):
        with self.lock:
            if not role:
                return
            for _, user in self.users.items():
                if role in user.roles:
                    user.refresh_last_update()

    def save(self):
        with self.lock:
            db = {
                'users': {un: u.to_dict() for un, u in self.users.items()},
                'roles': {rn: r.to_dict() for rn, r in self.roles.items()},
                'version': self.version
            }
            mgr.set_store(self.accessdb_config_key(), json.dumps(db))

    @classmethod
    def accessdb_config_key(cls, version=None):
        if version is None:
            version = cls.VERSION
        return "{}{}".format(cls.ACDB_CONFIG_KEY, version)

    def check_and_update_db(self):
        logger.debug("Checking for previous DB versions")

        def check_migrate_v1_to_current():
            # Check if version 1 exists in the DB and migrate it to current version
            v1_db = mgr.get_store(self.accessdb_config_key(1))
            if v1_db:
                logger.debug("Found database v1 credentials")
                v1_db = json.loads(v1_db)

                for user, _ in v1_db['users'].items():
                    v1_db['users'][user]['enabled'] = True
                    v1_db['users'][user]['pwdExpirationDate'] = None
                    v1_db['users'][user]['pwdUpdateRequired'] = False

                self.roles = {rn: Role.from_dict(r) for rn, r in v1_db.get('roles', {}).items()}
                self.users = {un: User.from_dict(u, dict(self.roles, **SYSTEM_ROLES))
                              for un, u in v1_db.get('users', {}).items()}

                self.save()

        check_migrate_v1_to_current()

    @classmethod
    def load(cls):
        logger.info("Loading user roles DB version=%s", cls.VERSION)

        json_db = mgr.get_store(cls.accessdb_config_key())
        if json_db is None:
            logger.debug("No DB v%s found, creating new...", cls.VERSION)
            db = cls(cls.VERSION, {}, {})
            # check if we can update from a previous version database
            db.check_and_update_db()
            return db

        dict_db = json.loads(json_db)
        roles = {rn: Role.from_dict(r)
                 for rn, r in dict_db.get('roles', {}).items()}
        users = {un: User.from_dict(u, dict(roles, **SYSTEM_ROLES))
                 for un, u in dict_db.get('users', {}).items()}
        return cls(dict_db['version'], users, roles)


def load_access_control_db():
    mgr.ACCESS_CTRL_DB = AccessControlDB.load()


# CLI dashboard access control scope commands

@CLIWriteCommand('dashboard set-login-credentials')
@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
def set_login_credentials_cmd(_, username: str, inbuf: str):
    '''
    Set the login credentials. Password read from -i <file>
    '''
    password = inbuf
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.set_password(password)
    except UserDoesNotExist:
        user = mgr.ACCESS_CTRL_DB.create_user(username, password, None, None)
        user.set_roles([ADMIN_ROLE])

    mgr.ACCESS_CTRL_DB.save()

    return 0, '''\
******************************************************************
***          WARNING: this command is deprecated.              ***
*** Please use the ac-user-* related commands to manage users. ***
******************************************************************
Username and password updated''', ''


@CLIReadCommand('dashboard ac-role-show')
def ac_role_show_cmd(_, rolename: Optional[str] = None):
    '''
    Show role info
    '''
    if not rolename:
        roles = dict(mgr.ACCESS_CTRL_DB.roles)
        roles.update(SYSTEM_ROLES)
        roles_list = [name for name, _ in roles.items()]
        return 0, json.dumps(roles_list), ''
    try:
        role = mgr.ACCESS_CTRL_DB.get_role(rolename)
    except RoleDoesNotExist as ex:
        if rolename not in SYSTEM_ROLES:
            return -errno.ENOENT, '', str(ex)
        role = SYSTEM_ROLES[rolename]
    return 0, json.dumps(role.to_dict()), ''


@CLIWriteCommand('dashboard ac-role-create')
def ac_role_create_cmd(_, rolename: str, description: Optional[str] = None):
    '''
    Create a new access control role
    '''
    try:
        role = mgr.ACCESS_CTRL_DB.create_role(rolename, description)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(role.to_dict()), ''
    except RoleAlreadyExists as ex:
        return -errno.EEXIST, '', str(ex)


@CLIWriteCommand('dashboard ac-role-delete')
def ac_role_delete_cmd(_, rolename: str):
    '''
    Delete an access control role
    '''
    try:
        mgr.ACCESS_CTRL_DB.delete_role(rolename)
        mgr.ACCESS_CTRL_DB.save()
        return 0, "Role '{}' deleted".format(rolename), ""
    except RoleDoesNotExist as ex:
        if rolename in SYSTEM_ROLES:
            return -errno.EPERM, '', "Cannot delete system role '{}'" \
                .format(rolename)
        return -errno.ENOENT, '', str(ex)
    except RoleIsAssociatedWithUser as ex:
        return -errno.EPERM, '', str(ex)


@CLIWriteCommand('dashboard ac-role-add-scope-perms')
def ac_role_add_scope_perms_cmd(_,
                                rolename: str,
                                scopename: str,
                                permissions: Sequence[str]):
    '''
    Add the scope permissions for a role
    '''
    try:
        role = mgr.ACCESS_CTRL_DB.get_role(rolename)
        perms_array = [perm.strip() for perm in permissions]
        role.set_scope_permissions(scopename, perms_array)
        mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(role.to_dict()), ''
    except RoleDoesNotExist as ex:
        if rolename in SYSTEM_ROLES:
            return -errno.EPERM, '', "Cannot update system role '{}'" \
                .format(rolename)
        return -errno.ENOENT, '', str(ex)
    except ScopeNotValid as ex:
        return -errno.EINVAL, '', str(ex) + "\n Possible values: {}" \
                                            .format(Scope.all_scopes())
    except PermissionNotValid as ex:
        return -errno.EINVAL, '', str(ex) + \
            "\n Possible values: {}" \
            .format(Permission.all_permissions())


@CLIWriteCommand('dashboard ac-role-del-scope-perms')
def ac_role_del_scope_perms_cmd(_, rolename: str, scopename: str):
    '''
    Delete the scope permissions for a role
    '''
    try:
        role = mgr.ACCESS_CTRL_DB.get_role(rolename)
        role.del_scope_permissions(scopename)
        mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(role.to_dict()), ''
    except RoleDoesNotExist as ex:
        if rolename in SYSTEM_ROLES:
            return -errno.EPERM, '', "Cannot update system role '{}'" \
                .format(rolename)
        return -errno.ENOENT, '', str(ex)
    except ScopeNotInRole as ex:
        return -errno.ENOENT, '', str(ex)


@CLIReadCommand('dashboard ac-user-show')
def ac_user_show_cmd(_, username: Optional[str] = None):
    '''
    Show user info
    '''
    if not username:
        users = mgr.ACCESS_CTRL_DB.users
        users_list = [name for name, _ in users.items()]
        return 0, json.dumps(users_list), ''
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-create')
@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
def ac_user_create_cmd(_, username: str, inbuf: str,
                       rolename: Optional[str] = None,
                       name: Optional[str] = None,
                       email: Optional[str] = None,
                       enabled: bool = True,
                       force_password: bool = False,
                       pwd_expiration_date: Optional[int] = None,
                       pwd_update_required: bool = False):
    '''
    Create a user. Password read from -i <file>
    '''
    password = inbuf
    try:
        role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None
    except RoleDoesNotExist as ex:
        if rolename not in SYSTEM_ROLES:
            return -errno.ENOENT, '', str(ex)
        role = SYSTEM_ROLES[rolename]

    try:
        if not force_password:
            pw_check = PasswordPolicy(password, username)
            pw_check.check_all()
        user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email,
                                              enabled, pwd_expiration_date,
                                              pwd_update_required)
    except PasswordPolicyException as ex:
        return -errno.EINVAL, '', str(ex)
    except UserAlreadyExists as ex:
        return 0, str(ex), ''

    if role:
        user.set_roles([role])
    mgr.ACCESS_CTRL_DB.save()
    return 0, json.dumps(user.to_dict()), ''


@CLIWriteCommand('dashboard ac-user-enable')
def ac_user_enable(_, username: str):
    '''
    Enable a user
    '''
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.enabled = True
        mgr.ACCESS_CTRL_DB.reset_attempt(username)

        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-disable')
def ac_user_disable(_, username: str):
    '''
    Disable a user
    '''
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.enabled = False

        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-delete')
def ac_user_delete_cmd(_, username: str):
    '''
    Delete user
    '''
    try:
        mgr.ACCESS_CTRL_DB.delete_user(username)
        mgr.ACCESS_CTRL_DB.save()
        return 0, "User '{}' deleted".format(username), ""
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-set-roles')
def ac_user_set_roles_cmd(_, username: str, roles: Sequence[str]):
    '''
    Set user roles
    '''
    rolesname = roles
    roles: List[Role] = []
    for rolename in rolesname:
        try:
            roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
        except RoleDoesNotExist as ex:
            if rolename not in SYSTEM_ROLES:
                return -errno.ENOENT, '', str(ex)
            roles.append(SYSTEM_ROLES[rolename])
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.set_roles(roles)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-add-roles')
def ac_user_add_roles_cmd(_, username: str, roles: Sequence[str]):
    '''
    Add roles to user
    '''
    rolesname = roles
    roles: List[Role] = []
    for rolename in rolesname:
        try:
            roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
        except RoleDoesNotExist as ex:
            if rolename not in SYSTEM_ROLES:
                return -errno.ENOENT, '', str(ex)
            roles.append(SYSTEM_ROLES[rolename])
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.add_roles(roles)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-del-roles')
def ac_user_del_roles_cmd(_, username: str, roles: Sequence[str]):
    '''
    Delete roles from user
    '''
    rolesname = roles
    roles: List[Role] = []
    for rolename in rolesname:
        try:
            roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
        except RoleDoesNotExist as ex:
            if rolename not in SYSTEM_ROLES:
                return -errno.ENOENT, '', str(ex)
            roles.append(SYSTEM_ROLES[rolename])
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.del_roles(roles)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)
    except RoleNotInUser as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-set-password')
@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
def ac_user_set_password(_, username: str, inbuf: str,
                         force_password: bool = False):
    '''
    Set user password from -i <file>
    '''
    password = inbuf
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        if not force_password:
            pw_check = PasswordPolicy(password, user.name)
            pw_check.check_all()
        user.set_password(password)
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except PasswordPolicyException as ex:
        return -errno.EINVAL, '', str(ex)
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-set-password-hash')
@CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
def ac_user_set_password_hash(_, username: str, inbuf: str):
    '''
    Set user password bcrypt hash from -i <file>
    '''
    hashed_password = inbuf
    try:
        # make sure the hashed_password is actually a bcrypt hash
        bcrypt.checkpw(b'', hashed_password.encode('utf-8'))
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        user.set_password_hash(hashed_password)

        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except ValueError:
        return -errno.EINVAL, '', 'Invalid password hash'
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


@CLIWriteCommand('dashboard ac-user-set-info')
def ac_user_set_info(_, username: str, name: str, email: str):
    '''
    Set user info
    '''
    try:
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        if name:
            user.name = name
        if email:
            user.email = email
        mgr.ACCESS_CTRL_DB.save()
        return 0, json.dumps(user.to_dict()), ''
    except UserDoesNotExist as ex:
        return -errno.ENOENT, '', str(ex)


class LocalAuthenticator(object):
    def __init__(self):
        load_access_control_db()

    def get_user(self, username):
        return mgr.ACCESS_CTRL_DB.get_user(username)

    def authenticate(self, username, password):
        try:
            user = mgr.ACCESS_CTRL_DB.get_user(username)
            if user.password:
                if user.enabled and user.compare_password(password) \
                   and not user.is_pwd_expired():
                    return {'permissions': user.permissions_dict(),
                            'pwdExpirationDate': user.pwd_expiration_date,
                            'pwdUpdateRequired': user.pwd_update_required}
        except UserDoesNotExist:
            logger.debug("User '%s' does not exist", username)
        return None

    def authorize(self, username, scope, permissions):
        user = mgr.ACCESS_CTRL_DB.get_user(username)
        return user.authorize(scope, permissions)