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

1"""Authentication provider for users stored in a JSON file. 

2 

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. 

8 

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. 

13 

14The command ``gws auth password`` asks for a password and prints its hash for 

15the file. 

16 

17Example:: 

18 

19 auth.providers+ { 

20 type "file" 

21 path "/data/users.json" 

22 } 

23 

24with ``/data/users.json``:: 

25 

26 [ 

27 { 

28 "login": "user_1", 

29 "password": "<hash>", 

30 "name": "User 1", 

31 "roles": ["editor"] 

32 } 

33 ] 

34""" 

35 

36import getpass 

37 

38import gws 

39import gws.base.auth 

40import gws.lib.jsonx 

41import gws.lib.password 

42 

43 

44@gws.ext.config.authProvider('file') 

45class Config(gws.base.auth.provider.Config): 

46 """Authentication against user records in a JSON file.""" 

47 

48 path: gws.FilePath 

49 """Path to the JSON file with user records.""" 

50 

51 

52@gws.ext.object.authProvider('file') 

53class Object(gws.base.auth.provider.Object): 

54 """File authentication provider.""" 

55 

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.""" 

62 

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)) 

67 

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 

73 

74 found = [rec for rec in self.db if gws.lib.password.compare(username, rec['login'])] 

75 

76 if len(found) > 1: 

77 raise gws.AuthenticationError(f'multiple entries for {username!r}') 

78 

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 

84 

85 if not gws.lib.password.check(password, found[0]['password']): 

86 raise gws.AuthenticationError(f'wrong password for {username!r}') 

87 

88 return self._make_user(found[0]) 

89 

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) 

94 

95 def _make_user(self, rec: dict): 

96 """Create a user from a file record.""" 

97 user_rec = dict(rec) 

98 

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', '') 

103 

104 return gws.base.auth.user.from_record(self, user_rec) 

105 

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.""" 

109 

110 while True: 

111 p1 = getpass.getpass('Password: ') 

112 p2 = getpass.getpass('Repeat : ') 

113 

114 if p1 != p2: 

115 print('passwords do not match') 

116 continue 

117 

118 p = gws.lib.password.encode(p1) 

119 print(p) 

120 break