Coverage for gws-app/gws/plugin/auth_provider/file/__init__.py: 81%
53 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Authentication provider for users stored in a JSON file.
3The file contains a list of user records (dicts). Each record must contain
4``login`` and ``password``, the password hashed with
5``gws.lib.password.encode``. ``name`` is used as the display name. Other
6fields, for example ``roles``, ``email`` or ``mfaUid``, are passed to
7``gws.base.auth.user.from_record`` and become properties of the user.
9The provider accepts ``username`` and ``password`` credentials. If the login
10is not found, a dummy hash is checked anyway, so that the response time does
11not reveal whether a login exists. The file is read once, at configuration
12time.
14The command ``gws auth password`` asks for a password and prints its hash for
15the file.
17Example::
19 auth.providers+ {
20 type "file"
21 path "/data/users.json"
22 }
24with ``/data/users.json``::
26 [
27 {
28 "login": "user_1",
29 "password": "<hash>",
30 "name": "User 1",
31 "roles": ["editor"]
32 }
33 ]
34"""
36import getpass
38import gws
39import gws.base.auth
40import gws.lib.jsonx
41import gws.lib.password
44@gws.ext.config.authProvider('file')
45class Config(gws.base.auth.provider.Config):
46 """Authentication against user records in a JSON file."""
48 path: gws.FilePath
49 """Path to the JSON file with user records."""
52@gws.ext.object.authProvider('file')
53class Object(gws.base.auth.provider.Object):
54 """File authentication provider."""
56 path: str
57 """Path to the JSON file."""
58 db: list[dict]
59 """User records read from the file."""
60 dummyPassword: str
61 """Hash of a random password, checked when a login is not found."""
63 def configure(self):
64 self.path = self.cfg('path')
65 self.db = gws.lib.jsonx.from_path(self.path)
66 self.dummyPassword = gws.lib.password.encode(gws.u.random_string(32))
68 def authenticate(self, method, credentials):
69 username = credentials.get('username')
70 password = credentials.get('password')
71 if not username or not password:
72 return
74 found = [rec for rec in self.db if gws.lib.password.compare(username, rec['login'])]
76 if len(found) > 1:
77 raise gws.AuthenticationError(f'multiple entries for {username!r}')
79 if not found:
80 # verify against a dummy hash, so that the time spent here
81 # does not reveal whether the login exists
82 gws.lib.password.check(password, self.dummyPassword)
83 return
85 if not gws.lib.password.check(password, found[0]['password']):
86 raise gws.AuthenticationError(f'wrong password for {username!r}')
88 return self._make_user(found[0])
90 def get_user(self, local_uid):
91 for rec in self.db:
92 if rec['login'] == local_uid:
93 return self._make_user(rec)
95 def _make_user(self, rec: dict):
96 """Create a user from a file record."""
97 user_rec = dict(rec)
99 login = user_rec.pop('login', '')
100 user_rec['localUid'] = user_rec['loginName'] = login
101 user_rec['displayName'] = user_rec.pop('name', login)
102 user_rec.pop('password', '')
104 return gws.base.auth.user.from_record(self, user_rec)
106 @gws.ext.command.cli('authPassword')
107 def passwd(self, p: gws.EmptyRequest):
108 """Ask for a password and print its hash for the users file."""
110 while True:
111 p1 = getpass.getpass('Password: ')
112 p2 = getpass.getpass('Repeat : ')
114 if p1 != p2:
115 print('passwords do not match')
116 continue
118 p = gws.lib.password.encode(p1)
119 print(p)
120 break