synapse 2.176.0__py311-none-any.whl → 2.177.0__py311-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of synapse might be problematic. Click here for more details.
- synapse/axon.py +24 -9
- synapse/cortex.py +329 -168
- synapse/cryotank.py +46 -37
- synapse/datamodel.py +17 -4
- synapse/exc.py +19 -0
- synapse/lib/agenda.py +7 -13
- synapse/lib/auth.py +1520 -0
- synapse/lib/cell.py +255 -53
- synapse/lib/grammar.py +5 -0
- synapse/lib/hive.py +24 -3
- synapse/lib/hiveauth.py +6 -32
- synapse/lib/layer.py +7 -4
- synapse/lib/link.py +21 -17
- synapse/lib/lmdbslab.py +149 -0
- synapse/lib/modelrev.py +1 -1
- synapse/lib/schemas.py +136 -0
- synapse/lib/storm.py +61 -29
- synapse/lib/stormlib/aha.py +1 -1
- synapse/lib/stormlib/auth.py +185 -10
- synapse/lib/stormlib/cortex.py +16 -5
- synapse/lib/stormlib/gen.py +80 -0
- synapse/lib/stormlib/model.py +55 -0
- synapse/lib/stormlib/modelext.py +60 -0
- synapse/lib/stormlib/tabular.py +212 -0
- synapse/lib/stormtypes.py +14 -1
- synapse/lib/trigger.py +1 -1
- synapse/lib/version.py +2 -2
- synapse/lib/view.py +55 -28
- synapse/models/base.py +7 -0
- synapse/models/biz.py +4 -0
- synapse/models/files.py +8 -1
- synapse/models/inet.py +8 -0
- synapse/tests/files/changelog/model_2.176.0_16ee721a6b7221344eaf946c3ab4602dda546b1a.yaml.gz +0 -0
- synapse/tests/files/changelog/model_2.176.0_2a25c58bbd344716cd7cbc3f4304d8925b0f4ef2.yaml.gz +0 -0
- synapse/tests/test_axon.py +7 -4
- synapse/tests/test_cortex.py +127 -82
- synapse/tests/test_cryotank.py +4 -4
- synapse/tests/test_datamodel.py +7 -0
- synapse/tests/test_lib_agenda.py +7 -0
- synapse/tests/{test_lib_hiveauth.py → test_lib_auth.py} +314 -11
- synapse/tests/test_lib_cell.py +161 -8
- synapse/tests/test_lib_httpapi.py +18 -14
- synapse/tests/test_lib_layer.py +33 -33
- synapse/tests/test_lib_link.py +42 -1
- synapse/tests/test_lib_lmdbslab.py +68 -0
- synapse/tests/test_lib_nexus.py +4 -4
- synapse/tests/test_lib_node.py +0 -7
- synapse/tests/test_lib_storm.py +45 -0
- synapse/tests/test_lib_stormlib_aha.py +1 -2
- synapse/tests/test_lib_stormlib_auth.py +21 -0
- synapse/tests/test_lib_stormlib_cortex.py +12 -12
- synapse/tests/test_lib_stormlib_gen.py +99 -0
- synapse/tests/test_lib_stormlib_model.py +108 -0
- synapse/tests/test_lib_stormlib_modelext.py +64 -0
- synapse/tests/test_lib_stormlib_tabular.py +226 -0
- synapse/tests/test_lib_stormsvc.py +4 -1
- synapse/tests/test_lib_stormtypes.py +10 -0
- synapse/tests/test_model_base.py +3 -0
- synapse/tests/test_model_biz.py +3 -0
- synapse/tests/test_model_files.py +12 -2
- synapse/tests/test_model_inet.py +24 -0
- synapse/tests/test_tools_changelog.py +196 -0
- synapse/tests/test_tools_healthcheck.py +4 -3
- synapse/tests/utils.py +1 -1
- synapse/tools/changelog.py +774 -15
- {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/METADATA +3 -3
- {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/RECORD +70 -64
- {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/WHEEL +1 -1
- {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/LICENSE +0 -0
- {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/top_level.txt +0 -0
synapse/lib/auth.py
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
import string
|
|
2
|
+
import logging
|
|
3
|
+
import dataclasses
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
import synapse.exc as s_exc
|
|
8
|
+
import synapse.common as s_common
|
|
9
|
+
|
|
10
|
+
import synapse.lib.cache as s_cache
|
|
11
|
+
import synapse.lib.nexus as s_nexus
|
|
12
|
+
import synapse.lib.msgpack as s_msgpack
|
|
13
|
+
import synapse.lib.schemas as s_schemas
|
|
14
|
+
|
|
15
|
+
import synapse.lib.crypto.passwd as s_passwd
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
def getShadow(passwd): # pragma: no cover
|
|
20
|
+
'''This API is deprecated.'''
|
|
21
|
+
s_common.deprecated('hiveauth.getShadow()', curv='2.110.0')
|
|
22
|
+
salt = s_common.guid()
|
|
23
|
+
hashed = s_common.guid((salt, passwd))
|
|
24
|
+
return (salt, hashed)
|
|
25
|
+
|
|
26
|
+
def textFromRule(rule):
|
|
27
|
+
text = '.'.join(rule[1])
|
|
28
|
+
if not rule[0]:
|
|
29
|
+
text = '!' + text
|
|
30
|
+
return text
|
|
31
|
+
|
|
32
|
+
@dataclasses.dataclass(slots=True)
|
|
33
|
+
class _allowedReason:
|
|
34
|
+
value: Union[bool | None]
|
|
35
|
+
default: bool = False
|
|
36
|
+
isadmin: bool = False
|
|
37
|
+
islocked: bool = False
|
|
38
|
+
gateiden: Union[str | None] = None
|
|
39
|
+
roleiden: Union[str | None] = None
|
|
40
|
+
rolename: Union[str | None] = None
|
|
41
|
+
rule: tuple = ()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def mesg(self):
|
|
45
|
+
if self.islocked:
|
|
46
|
+
return 'The user is locked.'
|
|
47
|
+
if self.default:
|
|
48
|
+
return 'No matching rule found.'
|
|
49
|
+
|
|
50
|
+
if self.isadmin:
|
|
51
|
+
if self.gateiden:
|
|
52
|
+
return f'The user is an admin of auth gate {self.gateiden}.'
|
|
53
|
+
return 'The user is a global admin.'
|
|
54
|
+
|
|
55
|
+
if self.rule:
|
|
56
|
+
rt = textFromRule((self.value, self.rule))
|
|
57
|
+
if self.gateiden:
|
|
58
|
+
if self.roleiden:
|
|
59
|
+
m = f'Matched role rule ({rt}) for role {self.rolename} on gate {self.gateiden}.'
|
|
60
|
+
else:
|
|
61
|
+
m = f'Matched user rule ({rt}) on gate {self.gateiden}.'
|
|
62
|
+
else:
|
|
63
|
+
if self.roleiden:
|
|
64
|
+
m = f'Matched role rule ({rt}) for role {self.rolename}.'
|
|
65
|
+
else:
|
|
66
|
+
m = f'Matched user rule ({rt}).'
|
|
67
|
+
return m
|
|
68
|
+
|
|
69
|
+
return 'No matching rule found.'
|
|
70
|
+
|
|
71
|
+
class Auth(s_nexus.Pusher):
|
|
72
|
+
'''
|
|
73
|
+
Auth is a user authentication and authorization stored in a Slab. Users
|
|
74
|
+
correspond to separate logins with different passwords and potentially
|
|
75
|
+
different privileges.
|
|
76
|
+
|
|
77
|
+
Users are assigned "rules". These rules are evaluated in order until a rule
|
|
78
|
+
matches. Each rule is a tuple of boolean, and a rule path (a sequence of
|
|
79
|
+
strings). Rules that are prefixes of a privilege match, i.e. a rule
|
|
80
|
+
('foo',) will match ('foo', 'bar').
|
|
81
|
+
|
|
82
|
+
Roles are just collections of rules. When a user is "granted" a role those
|
|
83
|
+
rules are assigned to that user. Unlike in an RBAC system, users don't
|
|
84
|
+
explicitly assume a role; they are merely a convenience mechanism to easily
|
|
85
|
+
assign the same rules to multiple users.
|
|
86
|
+
|
|
87
|
+
AuthGates are objects that manage their own authorization. Each
|
|
88
|
+
AuthGate has roles and users subkeys which contain rules specific to that
|
|
89
|
+
user or role for that AuthGate. The roles and users of an AuthGate,
|
|
90
|
+
called GateRole and GateUser respectively, contain the iden of a role or
|
|
91
|
+
user defined prior and rules specific to that role or user; they do not
|
|
92
|
+
duplicate the metadata of the role or user.
|
|
93
|
+
|
|
94
|
+
Layout::
|
|
95
|
+
|
|
96
|
+
Auth root (passed into constructor)
|
|
97
|
+
├ roles
|
|
98
|
+
│ ├ <role iden 1>
|
|
99
|
+
│ ├ ...
|
|
100
|
+
│ └ last role
|
|
101
|
+
├ users
|
|
102
|
+
│ ├ <user iden 1>
|
|
103
|
+
│ ├ ...
|
|
104
|
+
│ └ last user
|
|
105
|
+
└ authgates
|
|
106
|
+
├ <iden 1>
|
|
107
|
+
│ ├ roles
|
|
108
|
+
│ │ ├ <role iden 1>
|
|
109
|
+
│ │ ├ ...
|
|
110
|
+
│ │ └ last role
|
|
111
|
+
│ └ users
|
|
112
|
+
│ ├ <user iden 1>
|
|
113
|
+
│ ├ ...
|
|
114
|
+
│ └ last user
|
|
115
|
+
├ <iden 2>
|
|
116
|
+
│ ├ ...
|
|
117
|
+
└ ... last authgate
|
|
118
|
+
|
|
119
|
+
'''
|
|
120
|
+
|
|
121
|
+
async def __anit__(self, slab, dbname, pref='', nexsroot=None, seed=None, maxusers=0, policy=None):
|
|
122
|
+
'''
|
|
123
|
+
Args:
|
|
124
|
+
slab (s_lmdb.Slab): The slab to use for persistent storage for auth
|
|
125
|
+
dbname (str): The name of the db to use in the slab
|
|
126
|
+
'''
|
|
127
|
+
if policy:
|
|
128
|
+
s_schemas.reqValidPasswdPolicy(policy)
|
|
129
|
+
# Derive an iden from the db name
|
|
130
|
+
iden = f'auth:{dbname}'
|
|
131
|
+
await s_nexus.Pusher.__anit__(self, iden, nexsroot=nexsroot)
|
|
132
|
+
|
|
133
|
+
self.dbname = dbname
|
|
134
|
+
|
|
135
|
+
self.slab = slab
|
|
136
|
+
self.stor = self.slab.getSafeKeyVal(dbname)
|
|
137
|
+
|
|
138
|
+
if seed is None:
|
|
139
|
+
seed = s_common.guid()
|
|
140
|
+
|
|
141
|
+
self.maxusers = maxusers
|
|
142
|
+
self.policy = policy
|
|
143
|
+
|
|
144
|
+
self.userdefs = self.stor.getSubKeyVal('user:info:')
|
|
145
|
+
self.useridenbyname = self.stor.getSubKeyVal('user:name:')
|
|
146
|
+
self.userbyidencache = s_cache.FixedCache(self._getUser, size=1000)
|
|
147
|
+
self.useridenbynamecache = s_cache.FixedCache(self._getUserIden, size=1000)
|
|
148
|
+
|
|
149
|
+
self.roledefs = self.stor.getSubKeyVal('role:info:')
|
|
150
|
+
self.roleidenbyname = self.stor.getSubKeyVal('role:name:')
|
|
151
|
+
self.rolebyidencache = s_cache.FixedCache(self._getRole, size=1000)
|
|
152
|
+
self.roleidenbynamecache = s_cache.FixedCache(self._getRoleIden, size=1000)
|
|
153
|
+
|
|
154
|
+
self.gatedefs = self.stor.getSubKeyVal('gate:info:')
|
|
155
|
+
self.authgates = s_cache.FixedCache(self._getAuthGate, size=1000)
|
|
156
|
+
|
|
157
|
+
self.allrole = await self.getRoleByName('all')
|
|
158
|
+
if self.allrole is None:
|
|
159
|
+
# initialize the role of which all users are a member
|
|
160
|
+
guid = s_common.guid((seed, 'auth', 'role', 'all'))
|
|
161
|
+
await self._addRole(guid, 'all')
|
|
162
|
+
self.allrole = self.role(guid)
|
|
163
|
+
|
|
164
|
+
# initialize an admin user named root
|
|
165
|
+
self.rootuser = await self.getUserByName('root')
|
|
166
|
+
if self.rootuser is None:
|
|
167
|
+
guid = s_common.guid((seed, 'auth', 'user', 'root'))
|
|
168
|
+
await self._addUser(guid, 'root')
|
|
169
|
+
self.rootuser = self.user(guid)
|
|
170
|
+
|
|
171
|
+
await self.rootuser.setAdmin(True, logged=False)
|
|
172
|
+
await self.rootuser.setLocked(False, logged=False)
|
|
173
|
+
|
|
174
|
+
def users(self):
|
|
175
|
+
for useriden in self.useridenbyname.values():
|
|
176
|
+
userinfo = self.userdefs.get(useriden)
|
|
177
|
+
yield User(userinfo, self)
|
|
178
|
+
|
|
179
|
+
def roles(self):
|
|
180
|
+
for roleiden in self.roleidenbyname.values():
|
|
181
|
+
roleinfo = self.roledefs.get(roleiden)
|
|
182
|
+
yield Role(roleinfo, self)
|
|
183
|
+
|
|
184
|
+
def role(self, iden):
|
|
185
|
+
return self.rolebyidencache.get(iden)
|
|
186
|
+
|
|
187
|
+
def _getRole(self, iden):
|
|
188
|
+
roleinfo = self.roledefs.get(iden)
|
|
189
|
+
if roleinfo is not None:
|
|
190
|
+
return Role(roleinfo, self)
|
|
191
|
+
|
|
192
|
+
def user(self, iden):
|
|
193
|
+
return self.userbyidencache.get(iden)
|
|
194
|
+
|
|
195
|
+
def _getUser(self, iden):
|
|
196
|
+
userinfo = self.userdefs.get(iden)
|
|
197
|
+
if userinfo is not None:
|
|
198
|
+
return User(userinfo, self)
|
|
199
|
+
|
|
200
|
+
async def reqUser(self, iden):
|
|
201
|
+
user = self.user(iden)
|
|
202
|
+
if user is None:
|
|
203
|
+
mesg = f'No user with iden {iden}.'
|
|
204
|
+
raise s_exc.NoSuchUser(mesg=mesg, user=iden)
|
|
205
|
+
return user
|
|
206
|
+
|
|
207
|
+
async def reqRole(self, iden):
|
|
208
|
+
role = self.role(iden)
|
|
209
|
+
if role is None:
|
|
210
|
+
mesg = f'No role with iden {iden}.'
|
|
211
|
+
raise s_exc.NoSuchRole(mesg=mesg)
|
|
212
|
+
return role
|
|
213
|
+
|
|
214
|
+
async def reqUserByName(self, name):
|
|
215
|
+
user = await self.getUserByName(name)
|
|
216
|
+
if user is None:
|
|
217
|
+
mesg = f'No user named {name}.'
|
|
218
|
+
raise s_exc.NoSuchUser(mesg=mesg, username=name)
|
|
219
|
+
return user
|
|
220
|
+
|
|
221
|
+
async def reqUserByNameOrIden(self, name):
|
|
222
|
+
user = await self.getUserByName(name)
|
|
223
|
+
if user is not None:
|
|
224
|
+
return user
|
|
225
|
+
|
|
226
|
+
user = self.user(name)
|
|
227
|
+
if user is None:
|
|
228
|
+
mesg = f'No user with name or iden {name}.'
|
|
229
|
+
raise s_exc.NoSuchUser(mesg=mesg)
|
|
230
|
+
return user
|
|
231
|
+
|
|
232
|
+
async def reqRoleByName(self, name):
|
|
233
|
+
role = await self.getRoleByName(name)
|
|
234
|
+
if role is None:
|
|
235
|
+
mesg = f'No role named {name}.'
|
|
236
|
+
raise s_exc.NoSuchRole(mesg=mesg)
|
|
237
|
+
return role
|
|
238
|
+
|
|
239
|
+
async def getUserByName(self, name):
|
|
240
|
+
'''
|
|
241
|
+
Get a user by their username.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
name (str): Name of the user to get.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
User: A User. May return None if there is no user by the requested name.
|
|
248
|
+
'''
|
|
249
|
+
useriden = self.useridenbynamecache.get(name)
|
|
250
|
+
if useriden is not None:
|
|
251
|
+
return self.user(useriden)
|
|
252
|
+
|
|
253
|
+
async def getUserIdenByName(self, name):
|
|
254
|
+
return self.useridenbynamecache.get(name)
|
|
255
|
+
|
|
256
|
+
def _getUserIden(self, name):
|
|
257
|
+
return self.useridenbyname.get(name)
|
|
258
|
+
|
|
259
|
+
async def getRoleByName(self, name):
|
|
260
|
+
roleiden = self.roleidenbynamecache.get(name)
|
|
261
|
+
if roleiden is not None:
|
|
262
|
+
return self.role(roleiden)
|
|
263
|
+
|
|
264
|
+
def _getRoleIden(self, name):
|
|
265
|
+
return self.roleidenbyname.get(name)
|
|
266
|
+
|
|
267
|
+
@s_nexus.Pusher.onPushAuto('user:profile:set')
|
|
268
|
+
async def setUserProfileValu(self, iden, name, valu):
|
|
269
|
+
user = await self.reqUser(iden)
|
|
270
|
+
return user.profile.set(name, valu)
|
|
271
|
+
|
|
272
|
+
@s_nexus.Pusher.onPushAuto('user:profile:pop')
|
|
273
|
+
async def popUserProfileValu(self, iden, name, default=None):
|
|
274
|
+
user = await self.reqUser(iden)
|
|
275
|
+
return user.profile.pop(name, defv=default)
|
|
276
|
+
|
|
277
|
+
@s_nexus.Pusher.onPushAuto('user:var:set')
|
|
278
|
+
async def setUserVarValu(self, iden, name, valu):
|
|
279
|
+
user = await self.reqUser(iden)
|
|
280
|
+
return user.vars.set(name, valu)
|
|
281
|
+
|
|
282
|
+
@s_nexus.Pusher.onPushAuto('user:var:pop')
|
|
283
|
+
async def popUserVarValu(self, iden, name, default=None):
|
|
284
|
+
user = await self.reqUser(iden)
|
|
285
|
+
return user.vars.pop(name, defv=default)
|
|
286
|
+
|
|
287
|
+
@s_nexus.Pusher.onPushAuto('user:name')
|
|
288
|
+
async def setUserName(self, iden, name):
|
|
289
|
+
if not isinstance(name, str):
|
|
290
|
+
raise s_exc.BadArg(mesg='setUserName() name must be a string')
|
|
291
|
+
|
|
292
|
+
user = await self.getUserByName(name)
|
|
293
|
+
if user is not None:
|
|
294
|
+
if user.iden == iden:
|
|
295
|
+
return
|
|
296
|
+
raise s_exc.DupUserName(mesg=f'Duplicate username, {name=} already exists.', name=name)
|
|
297
|
+
|
|
298
|
+
user = await self.reqUser(iden)
|
|
299
|
+
|
|
300
|
+
if user.iden == self.rootuser.iden:
|
|
301
|
+
raise s_exc.BadArg(mesg='Cannot change the name of the root user.')
|
|
302
|
+
|
|
303
|
+
self.useridenbyname.set(name, iden)
|
|
304
|
+
self.useridenbyname.delete(user.name)
|
|
305
|
+
self.useridenbynamecache.pop(name)
|
|
306
|
+
self.useridenbynamecache.pop(user.name)
|
|
307
|
+
|
|
308
|
+
user.name = name
|
|
309
|
+
user.info['name'] = name
|
|
310
|
+
self.userdefs.set(iden, user.info)
|
|
311
|
+
|
|
312
|
+
beheld = {
|
|
313
|
+
'iden': iden,
|
|
314
|
+
'valu': name,
|
|
315
|
+
}
|
|
316
|
+
await self.feedBeholder('user:name', beheld)
|
|
317
|
+
|
|
318
|
+
@s_nexus.Pusher.onPushAuto('role:name')
|
|
319
|
+
async def setRoleName(self, iden, name):
|
|
320
|
+
if not isinstance(name, str):
|
|
321
|
+
raise s_exc.BadArg(mesg='setRoleName() name must be a string')
|
|
322
|
+
|
|
323
|
+
role = await self.getRoleByName(name)
|
|
324
|
+
if role is not None:
|
|
325
|
+
if role.iden == iden:
|
|
326
|
+
return
|
|
327
|
+
raise s_exc.DupRoleName(mesg=f'Duplicate role name, {name=} already exists.', name=name)
|
|
328
|
+
|
|
329
|
+
role = await self.reqRole(iden)
|
|
330
|
+
|
|
331
|
+
if role.name == 'all':
|
|
332
|
+
mesg = 'Role "all" may not be renamed.'
|
|
333
|
+
raise s_exc.BadArg(mesg=mesg)
|
|
334
|
+
|
|
335
|
+
self.roleidenbyname.set(name, iden)
|
|
336
|
+
self.roleidenbyname.delete(role.name)
|
|
337
|
+
self.roleidenbynamecache.pop(name)
|
|
338
|
+
self.roleidenbynamecache.pop(role.name)
|
|
339
|
+
|
|
340
|
+
role.name = name
|
|
341
|
+
role.info['name'] = name
|
|
342
|
+
self.roledefs.set(iden, role.info)
|
|
343
|
+
|
|
344
|
+
beheld = {
|
|
345
|
+
'iden': iden,
|
|
346
|
+
'valu': name,
|
|
347
|
+
}
|
|
348
|
+
await self.feedBeholder('role:name', beheld)
|
|
349
|
+
|
|
350
|
+
async def feedBeholder(self, evnt, info, gateiden=None, logged=True):
|
|
351
|
+
if self.nexsroot and self.nexsroot.started and logged:
|
|
352
|
+
behold = {
|
|
353
|
+
'event': evnt,
|
|
354
|
+
'offset': await self.nexsroot.index(),
|
|
355
|
+
'info': info
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if gateiden:
|
|
359
|
+
gate = self.getAuthGate(gateiden)
|
|
360
|
+
if gate:
|
|
361
|
+
behold['gates'] = [gate.pack()]
|
|
362
|
+
|
|
363
|
+
await self.fire('cell:beholder', **behold)
|
|
364
|
+
|
|
365
|
+
@s_nexus.Pusher.onPushAuto('user:info')
|
|
366
|
+
async def setUserInfo(self, iden, name, valu, gateiden=None, logged=True, mesg=None):
|
|
367
|
+
|
|
368
|
+
user = await self.reqUser(iden)
|
|
369
|
+
|
|
370
|
+
if gateiden is not None:
|
|
371
|
+
info = user.genGateInfo(gateiden)
|
|
372
|
+
info[name] = s_msgpack.deepcopy(valu)
|
|
373
|
+
gate = self.reqAuthGate(gateiden)
|
|
374
|
+
gate.users.set(iden, info)
|
|
375
|
+
|
|
376
|
+
user.info['authgates'][gateiden] = info
|
|
377
|
+
self.userdefs.set(iden, user.info)
|
|
378
|
+
else:
|
|
379
|
+
user.info[name] = s_msgpack.deepcopy(valu)
|
|
380
|
+
self.userdefs.set(iden, user.info)
|
|
381
|
+
|
|
382
|
+
if name in ('locked', 'archived') and not valu:
|
|
383
|
+
self.checkUserLimit()
|
|
384
|
+
|
|
385
|
+
if mesg is None:
|
|
386
|
+
mesg = {
|
|
387
|
+
'iden': iden,
|
|
388
|
+
'name': name,
|
|
389
|
+
}
|
|
390
|
+
if name != 'passwd':
|
|
391
|
+
mesg['valu'] = valu
|
|
392
|
+
|
|
393
|
+
await self.feedBeholder('user:info', mesg, gateiden=gateiden, logged=logged)
|
|
394
|
+
|
|
395
|
+
# since any user info *may* effect auth
|
|
396
|
+
user.clearAuthCache()
|
|
397
|
+
|
|
398
|
+
@s_nexus.Pusher.onPushAuto('role:info')
|
|
399
|
+
async def setRoleInfo(self, iden, name, valu, gateiden=None, logged=True, mesg=None):
|
|
400
|
+
role = await self.reqRole(iden)
|
|
401
|
+
|
|
402
|
+
if gateiden is not None:
|
|
403
|
+
info = role.genGateInfo(gateiden)
|
|
404
|
+
info[name] = s_msgpack.deepcopy(valu)
|
|
405
|
+
gate = self.reqAuthGate(gateiden)
|
|
406
|
+
gate.roles.set(iden, info)
|
|
407
|
+
|
|
408
|
+
role.info['authgates'][gateiden] = info
|
|
409
|
+
self.roledefs.set(iden, role.info)
|
|
410
|
+
else:
|
|
411
|
+
role.info[name] = s_msgpack.deepcopy(valu)
|
|
412
|
+
self.roledefs.set(iden, role.info)
|
|
413
|
+
|
|
414
|
+
if mesg is None:
|
|
415
|
+
mesg = {
|
|
416
|
+
'iden': iden,
|
|
417
|
+
'name': name,
|
|
418
|
+
'valu': valu,
|
|
419
|
+
}
|
|
420
|
+
await self.feedBeholder('role:info', mesg, gateiden=gateiden, logged=logged)
|
|
421
|
+
|
|
422
|
+
role.clearAuthCache()
|
|
423
|
+
|
|
424
|
+
async def addAuthGate(self, iden, authgatetype):
|
|
425
|
+
'''
|
|
426
|
+
Retrieve AuthGate by iden. Create if not present.
|
|
427
|
+
|
|
428
|
+
Note:
|
|
429
|
+
Not change distributed
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
(AuthGate)
|
|
433
|
+
'''
|
|
434
|
+
gate = self.getAuthGate(iden)
|
|
435
|
+
if gate is not None:
|
|
436
|
+
if gate.type != authgatetype:
|
|
437
|
+
raise s_exc.InconsistentStorage(mesg=f'Stored AuthGate is of type {gate.type}, not {authgatetype}')
|
|
438
|
+
return gate
|
|
439
|
+
|
|
440
|
+
info = {
|
|
441
|
+
'iden': iden,
|
|
442
|
+
'type': authgatetype
|
|
443
|
+
}
|
|
444
|
+
self.gatedefs.set(iden, info)
|
|
445
|
+
|
|
446
|
+
gate = AuthGate(info, self)
|
|
447
|
+
self.authgates.put(iden, gate)
|
|
448
|
+
|
|
449
|
+
return gate
|
|
450
|
+
|
|
451
|
+
async def delAuthGate(self, iden):
|
|
452
|
+
'''
|
|
453
|
+
Delete AuthGate by iden.
|
|
454
|
+
|
|
455
|
+
Note:
|
|
456
|
+
Not change distributed
|
|
457
|
+
'''
|
|
458
|
+
gate = self.getAuthGate(iden)
|
|
459
|
+
if gate is None:
|
|
460
|
+
raise s_exc.NoSuchAuthGate(iden=iden)
|
|
461
|
+
|
|
462
|
+
await gate.delete()
|
|
463
|
+
|
|
464
|
+
def getAuthGate(self, iden):
|
|
465
|
+
return self.authgates.get(iden)
|
|
466
|
+
|
|
467
|
+
def _getAuthGate(self, iden):
|
|
468
|
+
gateinfo = self.gatedefs.get(iden)
|
|
469
|
+
if gateinfo is not None:
|
|
470
|
+
return AuthGate(gateinfo, self)
|
|
471
|
+
|
|
472
|
+
def getAuthGates(self):
|
|
473
|
+
for gateinfo in self.gatedefs.values():
|
|
474
|
+
yield AuthGate(gateinfo, self)
|
|
475
|
+
|
|
476
|
+
def reqAuthGate(self, iden):
|
|
477
|
+
gate = self.authgates.get(iden)
|
|
478
|
+
if gate is None:
|
|
479
|
+
mesg = f'No auth gate found with iden: ({iden}).'
|
|
480
|
+
raise s_exc.NoSuchAuthGate(iden=iden, mesg=mesg)
|
|
481
|
+
return gate
|
|
482
|
+
|
|
483
|
+
def checkUserLimit(self):
|
|
484
|
+
'''
|
|
485
|
+
Check if we're at the specified user limit.
|
|
486
|
+
|
|
487
|
+
This should be called right before adding/unlocking/unarchiving a user.
|
|
488
|
+
|
|
489
|
+
Raises: s_exc.HitLimit if the number of active users is at the maximum.
|
|
490
|
+
'''
|
|
491
|
+
if self.maxusers == 0:
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
numusers = 0
|
|
495
|
+
|
|
496
|
+
for user in self.users():
|
|
497
|
+
if user.name == 'root':
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if user.isLocked() or user.isArchived():
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
numusers += 1
|
|
504
|
+
|
|
505
|
+
if numusers >= self.maxusers:
|
|
506
|
+
mesg = f'Cell at maximum number of users ({self.maxusers}).'
|
|
507
|
+
raise s_exc.HitLimit(mesg=mesg)
|
|
508
|
+
|
|
509
|
+
async def addUser(self, name, passwd=None, email=None, iden=None):
|
|
510
|
+
'''
|
|
511
|
+
Add a User to the Auth system.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
name (str): The name of the User.
|
|
515
|
+
passwd (str): A optional password for the user.
|
|
516
|
+
email (str): A optional email for the user.
|
|
517
|
+
iden (str): A optional iden to use as the user iden.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
User: A User.
|
|
521
|
+
'''
|
|
522
|
+
|
|
523
|
+
self.checkUserLimit()
|
|
524
|
+
|
|
525
|
+
if self.useridenbynamecache.get(name) is not None:
|
|
526
|
+
raise s_exc.DupUserName(mesg=f'Duplicate username, {name=} already exists.', name=name)
|
|
527
|
+
|
|
528
|
+
if iden is None:
|
|
529
|
+
iden = s_common.guid()
|
|
530
|
+
else:
|
|
531
|
+
if not s_common.isguid(iden):
|
|
532
|
+
raise s_exc.BadArg(name='iden', arg=iden, mesg='Argument it not a valid iden.')
|
|
533
|
+
|
|
534
|
+
if self.userdefs.get(iden) is not None:
|
|
535
|
+
raise s_exc.DupIden(name=name, iden=iden,
|
|
536
|
+
mesg='User already exists for the iden.')
|
|
537
|
+
|
|
538
|
+
await self._push('user:add', iden, name)
|
|
539
|
+
|
|
540
|
+
user = self.user(iden)
|
|
541
|
+
|
|
542
|
+
# Everyone's a member of 'all'
|
|
543
|
+
await user.grant(self.allrole.iden)
|
|
544
|
+
|
|
545
|
+
if email is not None:
|
|
546
|
+
await self.setUserInfo(user.iden, 'email', email)
|
|
547
|
+
|
|
548
|
+
if passwd is not None:
|
|
549
|
+
await user.setPasswd(passwd)
|
|
550
|
+
|
|
551
|
+
return user
|
|
552
|
+
|
|
553
|
+
@s_nexus.Pusher.onPush('user:add')
|
|
554
|
+
async def _addUser(self, iden, name):
|
|
555
|
+
|
|
556
|
+
user = self.useridenbynamecache.get(name)
|
|
557
|
+
if user is not None:
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
info = {
|
|
561
|
+
'iden': iden,
|
|
562
|
+
'name': name,
|
|
563
|
+
'admin': False,
|
|
564
|
+
'roles': (),
|
|
565
|
+
'rules': (),
|
|
566
|
+
'passwd': None,
|
|
567
|
+
'locked': False,
|
|
568
|
+
'archived': False,
|
|
569
|
+
'authgates': {},
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
self.userdefs.set(iden, info)
|
|
573
|
+
self.useridenbyname.set(name, iden)
|
|
574
|
+
|
|
575
|
+
user = User(info, self)
|
|
576
|
+
self.userbyidencache.put(iden, user)
|
|
577
|
+
self.useridenbynamecache.put(name, iden)
|
|
578
|
+
|
|
579
|
+
await self.feedBeholder('user:add', user.pack())
|
|
580
|
+
|
|
581
|
+
async def addRole(self, name, iden=None):
|
|
582
|
+
if self.roleidenbynamecache.get(name) is not None:
|
|
583
|
+
raise s_exc.DupRoleName(mesg=f'Duplicate role name, {name=} already exists.', name=name)
|
|
584
|
+
|
|
585
|
+
if iden is None:
|
|
586
|
+
iden = s_common.guid()
|
|
587
|
+
|
|
588
|
+
await self._push('role:add', iden, name)
|
|
589
|
+
|
|
590
|
+
return self.role(iden)
|
|
591
|
+
|
|
592
|
+
@s_nexus.Pusher.onPush('role:add')
|
|
593
|
+
async def _addRole(self, iden, name):
|
|
594
|
+
|
|
595
|
+
role = self.roleidenbynamecache.get(name)
|
|
596
|
+
if role is not None:
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
info = {
|
|
600
|
+
'iden': iden,
|
|
601
|
+
'name': name,
|
|
602
|
+
'admin': False,
|
|
603
|
+
'rules': (),
|
|
604
|
+
'authgates': {},
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
self.roledefs.set(iden, info)
|
|
608
|
+
self.roleidenbyname.set(name, iden)
|
|
609
|
+
|
|
610
|
+
role = Role(info, self)
|
|
611
|
+
self.rolebyidencache.put(iden, role)
|
|
612
|
+
self.roleidenbynamecache.put(name, iden)
|
|
613
|
+
|
|
614
|
+
await self.feedBeholder('role:add', role.pack())
|
|
615
|
+
|
|
616
|
+
async def delUser(self, iden):
|
|
617
|
+
|
|
618
|
+
await self.reqUser(iden)
|
|
619
|
+
return await self._push('user:del', iden)
|
|
620
|
+
|
|
621
|
+
@s_nexus.Pusher.onPush('user:del')
|
|
622
|
+
async def _delUser(self, iden):
|
|
623
|
+
|
|
624
|
+
if iden == self.rootuser.iden:
|
|
625
|
+
mesg = 'User "root" may not be deleted.'
|
|
626
|
+
raise s_exc.BadArg(mesg=mesg)
|
|
627
|
+
|
|
628
|
+
user = self.user(iden)
|
|
629
|
+
if user is None:
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
udef = user.pack()
|
|
633
|
+
self.userbyidencache.pop(user.iden)
|
|
634
|
+
self.useridenbynamecache.pop(user.name)
|
|
635
|
+
|
|
636
|
+
for gateiden in user.authgates.keys():
|
|
637
|
+
gate = self.getAuthGate(gateiden)
|
|
638
|
+
if gate is not None:
|
|
639
|
+
await gate._delGateUser(user.iden)
|
|
640
|
+
|
|
641
|
+
await user.vars.truncate()
|
|
642
|
+
await user.profile.truncate()
|
|
643
|
+
self.userdefs.delete(iden)
|
|
644
|
+
self.useridenbyname.delete(user.name)
|
|
645
|
+
|
|
646
|
+
await self.fire('user:del', udef=udef)
|
|
647
|
+
await self.feedBeholder('user:del', {'iden': iden})
|
|
648
|
+
|
|
649
|
+
def _getUsersInRole(self, role):
|
|
650
|
+
for user in self.users():
|
|
651
|
+
if role.iden in user.info.get('roles', ()):
|
|
652
|
+
yield user
|
|
653
|
+
|
|
654
|
+
async def delRole(self, iden):
|
|
655
|
+
await self.reqRole(iden)
|
|
656
|
+
return await self._push('role:del', iden)
|
|
657
|
+
|
|
658
|
+
@s_nexus.Pusher.onPush('role:del')
|
|
659
|
+
async def _delRole(self, iden):
|
|
660
|
+
|
|
661
|
+
if iden == self.allrole.iden:
|
|
662
|
+
mesg = 'Role "all" may not be deleted.'
|
|
663
|
+
raise s_exc.BadArg(mesg=mesg)
|
|
664
|
+
|
|
665
|
+
role = self.role(iden)
|
|
666
|
+
if role is None:
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
for user in self._getUsersInRole(role):
|
|
670
|
+
await user.revoke(role.iden, nexs=False)
|
|
671
|
+
|
|
672
|
+
for gateiden in role.authgates.keys():
|
|
673
|
+
gate = self.getAuthGate(gateiden)
|
|
674
|
+
if gate is not None:
|
|
675
|
+
await gate._delGateRole(role.iden)
|
|
676
|
+
|
|
677
|
+
self.rolebyidencache.pop(role.iden)
|
|
678
|
+
self.roleidenbynamecache.pop(role.name)
|
|
679
|
+
|
|
680
|
+
self.roledefs.delete(iden)
|
|
681
|
+
self.roleidenbyname.delete(role.name)
|
|
682
|
+
await self.feedBeholder('role:del', {'iden': iden})
|
|
683
|
+
|
|
684
|
+
class AuthGate():
|
|
685
|
+
'''
|
|
686
|
+
The storage object for object specific rules for users/roles.
|
|
687
|
+
'''
|
|
688
|
+
def __init__(self, info, auth):
|
|
689
|
+
|
|
690
|
+
self.auth = auth
|
|
691
|
+
|
|
692
|
+
self.iden = info.get('iden')
|
|
693
|
+
self.type = info.get('type')
|
|
694
|
+
|
|
695
|
+
self.gateroles = {} # iden -> role info
|
|
696
|
+
self.gateusers = {} # iden -> user info
|
|
697
|
+
|
|
698
|
+
self.users = auth.stor.getSubKeyVal(f'gate:{self.iden}:user:')
|
|
699
|
+
self.roles = auth.stor.getSubKeyVal(f'gate:{self.iden}:role:')
|
|
700
|
+
|
|
701
|
+
for useriden, userinfo in self.users.items():
|
|
702
|
+
self.gateusers[useriden] = userinfo
|
|
703
|
+
|
|
704
|
+
for roleiden, roleinfo in self.roles.items():
|
|
705
|
+
self.gateroles[roleiden] = roleinfo
|
|
706
|
+
|
|
707
|
+
def genUserInfo(self, iden):
|
|
708
|
+
userinfo = self.gateusers.get(iden)
|
|
709
|
+
if userinfo is not None: # pragma: no cover
|
|
710
|
+
return userinfo
|
|
711
|
+
|
|
712
|
+
self.gateusers[iden] = userinfo = {}
|
|
713
|
+
return userinfo
|
|
714
|
+
|
|
715
|
+
def genRoleInfo(self, iden):
|
|
716
|
+
roleinfo = self.gateroles.get(iden)
|
|
717
|
+
if roleinfo is not None: # pragma: no cover
|
|
718
|
+
return roleinfo
|
|
719
|
+
|
|
720
|
+
self.gateroles[iden] = roleinfo = {}
|
|
721
|
+
return roleinfo
|
|
722
|
+
|
|
723
|
+
async def _delGateUser(self, iden):
|
|
724
|
+
self.gateusers.pop(iden, None)
|
|
725
|
+
self.users.delete(iden)
|
|
726
|
+
|
|
727
|
+
async def _delGateRole(self, iden):
|
|
728
|
+
self.gateroles.pop(iden, None)
|
|
729
|
+
self.roles.delete(iden)
|
|
730
|
+
|
|
731
|
+
async def delete(self):
|
|
732
|
+
|
|
733
|
+
for useriden in self.gateusers.keys():
|
|
734
|
+
user = self.auth.user(useriden)
|
|
735
|
+
if user.authgates.pop(self.iden) is not None:
|
|
736
|
+
self.auth.userdefs.set(useriden, user.info)
|
|
737
|
+
user.clearAuthCache()
|
|
738
|
+
|
|
739
|
+
for roleiden in self.gateroles.keys():
|
|
740
|
+
role = self.auth.role(roleiden)
|
|
741
|
+
if role.authgates.pop(self.iden) is not None:
|
|
742
|
+
self.auth.roledefs.set(roleiden, role.info)
|
|
743
|
+
role.clearAuthCache()
|
|
744
|
+
|
|
745
|
+
self.auth.gatedefs.delete(self.iden)
|
|
746
|
+
self.auth.authgates.pop(self.iden)
|
|
747
|
+
await self.auth.stor.truncate(f'gate:{self.iden}:')
|
|
748
|
+
|
|
749
|
+
def pack(self):
|
|
750
|
+
users = []
|
|
751
|
+
for useriden, userinfo in self.gateusers.items():
|
|
752
|
+
users.append({
|
|
753
|
+
'iden': useriden,
|
|
754
|
+
'rules': userinfo.get('rules', ()),
|
|
755
|
+
'admin': userinfo.get('admin', False),
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
roles = []
|
|
759
|
+
for roleiden, roleinfo in self.gateroles.items():
|
|
760
|
+
roles.append({
|
|
761
|
+
'iden': roleiden,
|
|
762
|
+
'rules': roleinfo.get('rules', ()),
|
|
763
|
+
'admin': roleinfo.get('admin', False),
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
'iden': self.iden,
|
|
768
|
+
'type': self.type,
|
|
769
|
+
'users': users,
|
|
770
|
+
'roles': roles,
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
class Ruler():
|
|
774
|
+
'''
|
|
775
|
+
An object that holds a list of rules. This includes Users, Roles, and the AuthGate variants of those
|
|
776
|
+
'''
|
|
777
|
+
|
|
778
|
+
def __init__(self, info, auth):
|
|
779
|
+
|
|
780
|
+
self.auth = auth
|
|
781
|
+
self.info = info
|
|
782
|
+
self.name = info.get('name')
|
|
783
|
+
self.iden = info.get('iden')
|
|
784
|
+
|
|
785
|
+
self.authgates = info.get('authgates')
|
|
786
|
+
|
|
787
|
+
async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None): # pragma: no cover
|
|
788
|
+
raise s_exc.NoSuchImpl(mesg='Subclass must implement _setRulrInfo')
|
|
789
|
+
|
|
790
|
+
def getRules(self, gateiden=None):
|
|
791
|
+
|
|
792
|
+
if gateiden is None:
|
|
793
|
+
return list(self.info.get('rules', ()))
|
|
794
|
+
|
|
795
|
+
gateinfo = self.authgates.get(gateiden)
|
|
796
|
+
if gateinfo is None:
|
|
797
|
+
return []
|
|
798
|
+
|
|
799
|
+
return list(gateinfo.get('rules', ()))
|
|
800
|
+
|
|
801
|
+
async def setRules(self, rules, gateiden=None, nexs=True, mesg=None):
|
|
802
|
+
s_schemas.reqValidRules(rules)
|
|
803
|
+
return await self._setRulrInfo('rules', rules, gateiden=gateiden, nexs=nexs, mesg=mesg)
|
|
804
|
+
|
|
805
|
+
async def addRule(self, rule, indx=None, gateiden=None, nexs=True):
|
|
806
|
+
s_schemas.reqValidRules((rule,))
|
|
807
|
+
rules = self.getRules(gateiden=gateiden)
|
|
808
|
+
|
|
809
|
+
mesg = {
|
|
810
|
+
'name': 'rule:add',
|
|
811
|
+
'iden': self.iden,
|
|
812
|
+
'valu': rule,
|
|
813
|
+
}
|
|
814
|
+
if indx is None:
|
|
815
|
+
rules.append(rule)
|
|
816
|
+
else:
|
|
817
|
+
rules.insert(indx, rule)
|
|
818
|
+
mesg['indx'] = indx
|
|
819
|
+
|
|
820
|
+
await self.setRules(rules, gateiden=gateiden, nexs=nexs, mesg=mesg)
|
|
821
|
+
|
|
822
|
+
async def delRule(self, rule, gateiden=None):
|
|
823
|
+
s_schemas.reqValidRules((rule,))
|
|
824
|
+
rules = self.getRules(gateiden=gateiden)
|
|
825
|
+
if rule not in rules:
|
|
826
|
+
return False
|
|
827
|
+
|
|
828
|
+
mesg = {
|
|
829
|
+
'name': 'rule:del',
|
|
830
|
+
'iden': self.iden,
|
|
831
|
+
'valu': rule,
|
|
832
|
+
}
|
|
833
|
+
rules.remove(rule)
|
|
834
|
+
await self.setRules(rules, gateiden=gateiden, mesg=mesg)
|
|
835
|
+
return True
|
|
836
|
+
|
|
837
|
+
class Role(Ruler):
|
|
838
|
+
'''
|
|
839
|
+
A role within the authorization subsystem.
|
|
840
|
+
|
|
841
|
+
A role in Auth exists to bundle rules together so that the same
|
|
842
|
+
set of rules can be applied to multiple users.
|
|
843
|
+
'''
|
|
844
|
+
def pack(self):
|
|
845
|
+
return {
|
|
846
|
+
'type': 'role',
|
|
847
|
+
'iden': self.iden,
|
|
848
|
+
'name': self.name,
|
|
849
|
+
'rules': self.info.get('rules'),
|
|
850
|
+
'authgates': self.authgates,
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):
|
|
854
|
+
if nexs:
|
|
855
|
+
return await self.auth.setRoleInfo(self.iden, name, valu, gateiden=gateiden, mesg=mesg)
|
|
856
|
+
else:
|
|
857
|
+
return await self.auth._hndlsetRoleInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
|
|
858
|
+
|
|
859
|
+
async def setName(self, name):
|
|
860
|
+
return await self.auth.setRoleName(self.iden, name)
|
|
861
|
+
|
|
862
|
+
def clearAuthCache(self):
|
|
863
|
+
for user in self.auth.userbyidencache.cache.values():
|
|
864
|
+
if user is not None and user.hasRole(self.iden):
|
|
865
|
+
user.clearAuthCache()
|
|
866
|
+
|
|
867
|
+
def genGateInfo(self, gateiden):
|
|
868
|
+
info = self.authgates.get(gateiden)
|
|
869
|
+
if info is None:
|
|
870
|
+
gate = self.auth.reqAuthGate(gateiden)
|
|
871
|
+
info = self.authgates[gateiden] = gate.genRoleInfo(self.iden)
|
|
872
|
+
return info
|
|
873
|
+
|
|
874
|
+
def allowed(self, perm, default=None, gateiden=None):
|
|
875
|
+
|
|
876
|
+
perm = tuple(perm)
|
|
877
|
+
if gateiden is not None:
|
|
878
|
+
info = self.authgates.get(gateiden)
|
|
879
|
+
if info is not None:
|
|
880
|
+
for allow, path in info.get('rules', ()):
|
|
881
|
+
if perm[:len(path)] == path:
|
|
882
|
+
return allow
|
|
883
|
+
return default
|
|
884
|
+
|
|
885
|
+
# 2. check role rules
|
|
886
|
+
for allow, path in self.info.get('rules', ()):
|
|
887
|
+
if perm[:len(path)] == path:
|
|
888
|
+
return allow
|
|
889
|
+
|
|
890
|
+
return default
|
|
891
|
+
|
|
892
|
+
class User(Ruler):
|
|
893
|
+
'''
|
|
894
|
+
A user (could be human or computer) of the system within Auth.
|
|
895
|
+
|
|
896
|
+
Cortex-wide rules are stored here. AuthGate-specific rules for this user are stored in an GateUser.
|
|
897
|
+
'''
|
|
898
|
+
def __init__(self, info, auth):
|
|
899
|
+
Ruler.__init__(self, info, auth)
|
|
900
|
+
|
|
901
|
+
self.vars = auth.stor.getSubKeyVal(f'user:{self.iden}:vars:')
|
|
902
|
+
self.profile = auth.stor.getSubKeyVal(f'user:{self.iden}:profile:')
|
|
903
|
+
|
|
904
|
+
self.permcache = s_cache.FixedCache(self._allowed)
|
|
905
|
+
self.allowedcache = s_cache.FixedCache(self._getAllowedReason)
|
|
906
|
+
|
|
907
|
+
def pack(self, packroles=False):
|
|
908
|
+
|
|
909
|
+
roles = self.info.get('roles', ())
|
|
910
|
+
if packroles:
|
|
911
|
+
_roles = []
|
|
912
|
+
for r in roles:
|
|
913
|
+
role = self.auth.role(r)
|
|
914
|
+
if role is None:
|
|
915
|
+
logger.error(f'User {self.iden} ({self.name}) contains a missing role: {r}')
|
|
916
|
+
continue
|
|
917
|
+
_roles.append(role.pack())
|
|
918
|
+
roles = _roles
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
'type': 'user',
|
|
922
|
+
'iden': self.iden,
|
|
923
|
+
'name': self.name,
|
|
924
|
+
'rules': self.info.get('rules'),
|
|
925
|
+
'roles': roles,
|
|
926
|
+
'admin': self.info.get('admin'),
|
|
927
|
+
'email': self.info.get('email'),
|
|
928
|
+
'locked': self.info.get('locked'),
|
|
929
|
+
'archived': self.info.get('archived'),
|
|
930
|
+
'authgates': self.authgates,
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):
|
|
934
|
+
if nexs:
|
|
935
|
+
return await self.auth.setUserInfo(self.iden, name, valu, gateiden=gateiden, mesg=mesg)
|
|
936
|
+
else:
|
|
937
|
+
return await self.auth._hndlsetUserInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
|
|
938
|
+
|
|
939
|
+
async def setName(self, name):
|
|
940
|
+
return await self.auth.setUserName(self.iden, name)
|
|
941
|
+
|
|
942
|
+
async def setProfileValu(self, name, valu):
|
|
943
|
+
return await self.auth.setUserProfileValu(self.iden, name, valu)
|
|
944
|
+
|
|
945
|
+
async def popProfileValu(self, name, default=None):
|
|
946
|
+
return await self.auth.popUserProfileValu(self.iden, name, default=default)
|
|
947
|
+
|
|
948
|
+
async def setVarValu(self, name, valu):
|
|
949
|
+
return await self.auth.setUserVarValu(self.iden, name, valu)
|
|
950
|
+
|
|
951
|
+
async def popVarValu(self, name, default=None):
|
|
952
|
+
return await self.auth.popUserVarValu(self.iden, name, default=default)
|
|
953
|
+
|
|
954
|
+
async def allow(self, perm):
|
|
955
|
+
if not self.allowed(perm):
|
|
956
|
+
await self.addRule((True, perm), indx=0)
|
|
957
|
+
|
|
958
|
+
def allowed(self,
|
|
959
|
+
perm: tuple[str, ...],
|
|
960
|
+
default: Optional[str] = None,
|
|
961
|
+
gateiden: Optional[str] = None,
|
|
962
|
+
deepdeny: bool = False) -> Union[bool, None]:
|
|
963
|
+
'''
|
|
964
|
+
Check if a user is allowed a given permission.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
perm: The permission tuple to check.
|
|
968
|
+
default: The default rule value if there is no match.
|
|
969
|
+
gateiden: The gate iden to check against.
|
|
970
|
+
deepdeny: If True, give precedence for checking deny rules which are more specific than the requested
|
|
971
|
+
permission.
|
|
972
|
+
|
|
973
|
+
Notes:
|
|
974
|
+
The use of the deepdeny argument is intended for checking a less-specific part of a permissions tree, in
|
|
975
|
+
order to know about possible short circuit options. Using it to check a more specific part may have
|
|
976
|
+
unintended results.
|
|
977
|
+
|
|
978
|
+
Returns:
|
|
979
|
+
The allowed value of the permission.
|
|
980
|
+
'''
|
|
981
|
+
perm = tuple(perm)
|
|
982
|
+
return self.permcache.get((perm, default, gateiden, deepdeny))
|
|
983
|
+
|
|
984
|
+
def _allowed(self, pkey):
|
|
985
|
+
'''
|
|
986
|
+
NOTE: This must remain in sync with any changes to _getAllowedReason()!
|
|
987
|
+
'''
|
|
988
|
+
perm, default, gateiden, deepdeny = pkey
|
|
989
|
+
|
|
990
|
+
if self.info.get('locked'):
|
|
991
|
+
return False
|
|
992
|
+
|
|
993
|
+
if self.info.get('admin'):
|
|
994
|
+
return True
|
|
995
|
+
|
|
996
|
+
if deepdeny and self._hasDeepDeny(perm, gateiden):
|
|
997
|
+
return False
|
|
998
|
+
|
|
999
|
+
# 1. check authgate user rules
|
|
1000
|
+
if gateiden is not None:
|
|
1001
|
+
|
|
1002
|
+
info = self.authgates.get(gateiden)
|
|
1003
|
+
if info is not None:
|
|
1004
|
+
|
|
1005
|
+
if info.get('admin'):
|
|
1006
|
+
return True
|
|
1007
|
+
|
|
1008
|
+
for allow, path in info.get('rules', ()):
|
|
1009
|
+
if perm[:len(path)] == path:
|
|
1010
|
+
return allow
|
|
1011
|
+
|
|
1012
|
+
# 2. check user rules
|
|
1013
|
+
for allow, path in self.info.get('rules', ()):
|
|
1014
|
+
if perm[:len(path)] == path:
|
|
1015
|
+
return allow
|
|
1016
|
+
|
|
1017
|
+
# 3. check authgate role rules
|
|
1018
|
+
if gateiden is not None:
|
|
1019
|
+
|
|
1020
|
+
for role in self.getRoles():
|
|
1021
|
+
|
|
1022
|
+
info = role.authgates.get(gateiden)
|
|
1023
|
+
if info is None:
|
|
1024
|
+
continue
|
|
1025
|
+
|
|
1026
|
+
for allow, path in info.get('rules', ()):
|
|
1027
|
+
if perm[:len(path)] == path:
|
|
1028
|
+
return allow
|
|
1029
|
+
|
|
1030
|
+
# 4. check role rules
|
|
1031
|
+
for role in self.getRoles():
|
|
1032
|
+
for allow, path in role.info.get('rules', ()):
|
|
1033
|
+
if perm[:len(path)] == path:
|
|
1034
|
+
return allow
|
|
1035
|
+
|
|
1036
|
+
return default
|
|
1037
|
+
|
|
1038
|
+
def getAllowedReason(self, perm, default=None, gateiden=None):
|
|
1039
|
+
'''
|
|
1040
|
+
A routine which will return a tuple of (allowed, info).
|
|
1041
|
+
'''
|
|
1042
|
+
perm = tuple(perm)
|
|
1043
|
+
return self.allowedcache.get((perm, default, gateiden))
|
|
1044
|
+
|
|
1045
|
+
def _getAllowedReason(self, pkey):
|
|
1046
|
+
'''
|
|
1047
|
+
NOTE: This must remain in sync with any changes to _allowed()!
|
|
1048
|
+
'''
|
|
1049
|
+
perm, default, gateiden = pkey
|
|
1050
|
+
if self.info.get('locked'):
|
|
1051
|
+
return _allowedReason(False, islocked=True)
|
|
1052
|
+
|
|
1053
|
+
if self.info.get('admin'):
|
|
1054
|
+
return _allowedReason(True, isadmin=True)
|
|
1055
|
+
|
|
1056
|
+
# 1. check authgate user rules
|
|
1057
|
+
if gateiden is not None:
|
|
1058
|
+
|
|
1059
|
+
info = self.authgates.get(gateiden)
|
|
1060
|
+
if info is not None:
|
|
1061
|
+
|
|
1062
|
+
if info.get('admin'):
|
|
1063
|
+
return _allowedReason(True, isadmin=True, gateiden=gateiden)
|
|
1064
|
+
|
|
1065
|
+
for allow, path in info.get('rules', ()):
|
|
1066
|
+
if perm[:len(path)] == path:
|
|
1067
|
+
return _allowedReason(allow, gateiden=gateiden, rule=path)
|
|
1068
|
+
|
|
1069
|
+
# 2. check user rules
|
|
1070
|
+
for allow, path in self.info.get('rules', ()):
|
|
1071
|
+
if perm[:len(path)] == path:
|
|
1072
|
+
return _allowedReason(allow, rule=path)
|
|
1073
|
+
|
|
1074
|
+
# 3. check authgate role rules
|
|
1075
|
+
if gateiden is not None:
|
|
1076
|
+
|
|
1077
|
+
for role in self.getRoles():
|
|
1078
|
+
|
|
1079
|
+
info = role.authgates.get(gateiden)
|
|
1080
|
+
if info is None:
|
|
1081
|
+
continue
|
|
1082
|
+
|
|
1083
|
+
for allow, path in info.get('rules', ()):
|
|
1084
|
+
if perm[:len(path)] == path:
|
|
1085
|
+
return _allowedReason(allow, gateiden=gateiden, roleiden=role.iden, rolename=role.name,
|
|
1086
|
+
rule=path)
|
|
1087
|
+
|
|
1088
|
+
# 4. check role rules
|
|
1089
|
+
for role in self.getRoles():
|
|
1090
|
+
for allow, path in role.info.get('rules', ()):
|
|
1091
|
+
if perm[:len(path)] == path:
|
|
1092
|
+
return _allowedReason(allow, roleiden=role.iden, rolename=role.name, rule=path)
|
|
1093
|
+
|
|
1094
|
+
return _allowedReason(default, default=True)
|
|
1095
|
+
|
|
1096
|
+
def _hasDeepDeny(self, perm, gateiden):
|
|
1097
|
+
|
|
1098
|
+
permlen = len(perm)
|
|
1099
|
+
|
|
1100
|
+
# 1. check authgate user rules
|
|
1101
|
+
if gateiden is not None:
|
|
1102
|
+
|
|
1103
|
+
info = self.authgates.get(gateiden)
|
|
1104
|
+
if info is not None:
|
|
1105
|
+
|
|
1106
|
+
if info.get('admin'):
|
|
1107
|
+
return False
|
|
1108
|
+
|
|
1109
|
+
for allow, path in info.get('rules', ()):
|
|
1110
|
+
if allow:
|
|
1111
|
+
continue
|
|
1112
|
+
if path[:permlen] == perm and len(path) > permlen:
|
|
1113
|
+
return True
|
|
1114
|
+
|
|
1115
|
+
# 2. check user rules
|
|
1116
|
+
for allow, path in self.info.get('rules', ()):
|
|
1117
|
+
if allow:
|
|
1118
|
+
continue
|
|
1119
|
+
|
|
1120
|
+
if path[:permlen] == perm and len(path) > permlen:
|
|
1121
|
+
return True
|
|
1122
|
+
|
|
1123
|
+
# 3. check authgate role rules
|
|
1124
|
+
if gateiden is not None:
|
|
1125
|
+
|
|
1126
|
+
for role in self.getRoles():
|
|
1127
|
+
|
|
1128
|
+
info = role.authgates.get(gateiden)
|
|
1129
|
+
if info is None:
|
|
1130
|
+
continue
|
|
1131
|
+
|
|
1132
|
+
for allow, path in info.get('rules', ()):
|
|
1133
|
+
if allow:
|
|
1134
|
+
continue
|
|
1135
|
+
if path[:permlen] == perm and len(path) > permlen:
|
|
1136
|
+
return True
|
|
1137
|
+
|
|
1138
|
+
# 4. check role rules
|
|
1139
|
+
for role in self.getRoles():
|
|
1140
|
+
for allow, path in role.info.get('rules', ()):
|
|
1141
|
+
if allow:
|
|
1142
|
+
continue
|
|
1143
|
+
if path[:permlen] == perm and len(path) > permlen:
|
|
1144
|
+
return True
|
|
1145
|
+
|
|
1146
|
+
return False
|
|
1147
|
+
|
|
1148
|
+
def clearAuthCache(self):
|
|
1149
|
+
self.permcache.clear()
|
|
1150
|
+
self.allowedcache.clear()
|
|
1151
|
+
|
|
1152
|
+
def genGateInfo(self, gateiden):
|
|
1153
|
+
info = self.authgates.get(gateiden)
|
|
1154
|
+
if info is None:
|
|
1155
|
+
gate = self.auth.reqAuthGate(gateiden)
|
|
1156
|
+
info = gate.genUserInfo(self.iden)
|
|
1157
|
+
return info
|
|
1158
|
+
|
|
1159
|
+
def confirm(self, perm, default=None, gateiden=None):
|
|
1160
|
+
if not self.allowed(perm, default=default, gateiden=gateiden):
|
|
1161
|
+
self.raisePermDeny(perm, gateiden=gateiden)
|
|
1162
|
+
|
|
1163
|
+
def raisePermDeny(self, perm, gateiden=None):
|
|
1164
|
+
|
|
1165
|
+
perm = '.'.join(perm)
|
|
1166
|
+
if gateiden is None:
|
|
1167
|
+
mesg = f'User {self.name!r} ({self.iden}) must have permission {perm}'
|
|
1168
|
+
raise s_exc.AuthDeny(mesg=mesg, perm=perm, user=self.iden, username=self.name)
|
|
1169
|
+
|
|
1170
|
+
gate = self.auth.reqAuthGate(gateiden)
|
|
1171
|
+
mesg = f'User {self.name!r} ({self.iden}) must have permission {perm} on object {gate.iden} ({gate.type}).'
|
|
1172
|
+
raise s_exc.AuthDeny(mesg=mesg, perm=perm, user=self.iden, username=self.name)
|
|
1173
|
+
|
|
1174
|
+
def getRoles(self):
|
|
1175
|
+
for iden in self.info.get('roles', ()):
|
|
1176
|
+
role = self.auth.role(iden)
|
|
1177
|
+
if role is None:
|
|
1178
|
+
logger.warning(f'user {self.iden} has non-existent role: {iden}')
|
|
1179
|
+
continue
|
|
1180
|
+
yield role
|
|
1181
|
+
|
|
1182
|
+
def hasRole(self, iden):
|
|
1183
|
+
return iden in self.info.get('roles', ())
|
|
1184
|
+
|
|
1185
|
+
async def grant(self, roleiden, indx=None):
|
|
1186
|
+
|
|
1187
|
+
role = await self.auth.reqRole(roleiden)
|
|
1188
|
+
|
|
1189
|
+
roles = list(self.info.get('roles'))
|
|
1190
|
+
if role.iden in roles:
|
|
1191
|
+
return
|
|
1192
|
+
|
|
1193
|
+
if indx is None:
|
|
1194
|
+
roles.append(role.iden)
|
|
1195
|
+
else:
|
|
1196
|
+
roles.insert(indx, role.iden)
|
|
1197
|
+
|
|
1198
|
+
mesg = {'name': 'role:grant', 'iden': self.iden, 'role': role.pack()}
|
|
1199
|
+
await self.auth.setUserInfo(self.iden, 'roles', roles, mesg=mesg)
|
|
1200
|
+
|
|
1201
|
+
async def setRoles(self, roleidens):
|
|
1202
|
+
'''
|
|
1203
|
+
Replace all the roles for a given user with a new list of roles.
|
|
1204
|
+
|
|
1205
|
+
Args:
|
|
1206
|
+
roleidens (list): A list of roleidens.
|
|
1207
|
+
|
|
1208
|
+
Notes:
|
|
1209
|
+
The roleiden for the "all" role must be present in the new list of roles. This replaces all existing roles
|
|
1210
|
+
that the user has with the new roles.
|
|
1211
|
+
|
|
1212
|
+
Returns:
|
|
1213
|
+
None
|
|
1214
|
+
'''
|
|
1215
|
+
current_roles = list(self.info.get('roles'))
|
|
1216
|
+
|
|
1217
|
+
roleidens = list(roleidens)
|
|
1218
|
+
|
|
1219
|
+
if current_roles == roleidens:
|
|
1220
|
+
return
|
|
1221
|
+
|
|
1222
|
+
if self.auth.allrole.iden not in roleidens:
|
|
1223
|
+
mesg = 'Role "all" must be in the list of roles set.'
|
|
1224
|
+
raise s_exc.BadArg(mesg=mesg)
|
|
1225
|
+
|
|
1226
|
+
roles = []
|
|
1227
|
+
for iden in roleidens:
|
|
1228
|
+
r = await self.auth.reqRole(iden)
|
|
1229
|
+
roles.append(r.pack())
|
|
1230
|
+
|
|
1231
|
+
mesg = {'name': 'role:set', 'iden': self.iden, 'roles': roles}
|
|
1232
|
+
await self.auth.setUserInfo(self.iden, 'roles', roleidens, mesg=mesg)
|
|
1233
|
+
|
|
1234
|
+
async def revoke(self, iden, nexs=True):
|
|
1235
|
+
|
|
1236
|
+
role = await self.auth.reqRole(iden)
|
|
1237
|
+
|
|
1238
|
+
if role.name == 'all':
|
|
1239
|
+
mesg = 'Role "all" may not be revoked.'
|
|
1240
|
+
raise s_exc.BadArg(mesg=mesg)
|
|
1241
|
+
|
|
1242
|
+
roles = list(self.info.get('roles'))
|
|
1243
|
+
if role.iden not in roles:
|
|
1244
|
+
return
|
|
1245
|
+
|
|
1246
|
+
roles.remove(role.iden)
|
|
1247
|
+
mesg = {'name': 'role:revoke', 'iden': self.iden, 'role': role.pack()}
|
|
1248
|
+
if nexs:
|
|
1249
|
+
await self.auth.setUserInfo(self.iden, 'roles', roles, mesg=mesg)
|
|
1250
|
+
else:
|
|
1251
|
+
await self.auth._hndlsetUserInfo(self.iden, 'roles', roles, logged=nexs, mesg=mesg)
|
|
1252
|
+
|
|
1253
|
+
def isLocked(self):
|
|
1254
|
+
return self.info.get('locked')
|
|
1255
|
+
|
|
1256
|
+
def isAdmin(self, gateiden=None):
|
|
1257
|
+
|
|
1258
|
+
# being a global admin always wins
|
|
1259
|
+
admin = self.info.get('admin', False)
|
|
1260
|
+
if admin or gateiden is None:
|
|
1261
|
+
return admin
|
|
1262
|
+
|
|
1263
|
+
gateinfo = self.authgates.get(gateiden)
|
|
1264
|
+
if gateinfo is None:
|
|
1265
|
+
return False
|
|
1266
|
+
|
|
1267
|
+
return gateinfo.get('admin', False)
|
|
1268
|
+
|
|
1269
|
+
def reqAdmin(self, gateiden=None, mesg=None):
|
|
1270
|
+
|
|
1271
|
+
if self.isAdmin(gateiden=gateiden):
|
|
1272
|
+
return
|
|
1273
|
+
|
|
1274
|
+
if mesg is None:
|
|
1275
|
+
mesg = 'This action requires global admin permissions.'
|
|
1276
|
+
if gateiden is not None:
|
|
1277
|
+
mesg = f'This action requires admin permissions on gate: {gateiden}'
|
|
1278
|
+
|
|
1279
|
+
raise s_exc.AuthDeny(mesg=mesg, user=self.iden, username=self.name)
|
|
1280
|
+
|
|
1281
|
+
def isArchived(self):
|
|
1282
|
+
return self.info.get('archived')
|
|
1283
|
+
|
|
1284
|
+
async def setAdmin(self, admin, gateiden=None, logged=True):
|
|
1285
|
+
if not isinstance(admin, bool):
|
|
1286
|
+
raise s_exc.BadArg(mesg='setAdmin requires a boolean')
|
|
1287
|
+
|
|
1288
|
+
if self.iden == self.auth.rootuser.iden and not admin:
|
|
1289
|
+
raise s_exc.BadArg(mesg='Cannot remove admin from root user.')
|
|
1290
|
+
|
|
1291
|
+
if logged:
|
|
1292
|
+
await self.auth.setUserInfo(self.iden, 'admin', admin, gateiden=gateiden)
|
|
1293
|
+
else:
|
|
1294
|
+
await self.auth._hndlsetUserInfo(self.iden, 'admin', admin, gateiden=gateiden, logged=logged)
|
|
1295
|
+
|
|
1296
|
+
async def setLocked(self, locked, logged=True):
|
|
1297
|
+
if not isinstance(locked, bool):
|
|
1298
|
+
raise s_exc.BadArg(mesg='setLocked requires a boolean')
|
|
1299
|
+
|
|
1300
|
+
if self.iden == self.auth.rootuser.iden and locked:
|
|
1301
|
+
raise s_exc.BadArg(mesg='Cannot lock admin root user.')
|
|
1302
|
+
|
|
1303
|
+
resetAttempts = (
|
|
1304
|
+
not locked and
|
|
1305
|
+
self.info.get('policy:attempts', 0) > 0
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
if logged:
|
|
1309
|
+
await self.auth.setUserInfo(self.iden, 'locked', locked)
|
|
1310
|
+
if resetAttempts:
|
|
1311
|
+
await self.auth.setUserInfo(self.iden, 'policy:attempts', 0)
|
|
1312
|
+
|
|
1313
|
+
else:
|
|
1314
|
+
await self.auth._hndlsetUserInfo(self.iden, 'locked', locked, logged=logged)
|
|
1315
|
+
if resetAttempts:
|
|
1316
|
+
await self.auth._hndlsetUserInfo(self.iden, 'policy:attempts', 0)
|
|
1317
|
+
|
|
1318
|
+
async def setArchived(self, archived):
|
|
1319
|
+
if not isinstance(archived, bool):
|
|
1320
|
+
raise s_exc.BadArg(mesg='setArchived requires a boolean')
|
|
1321
|
+
|
|
1322
|
+
if self.iden == self.auth.rootuser.iden and archived:
|
|
1323
|
+
raise s_exc.BadArg(mesg='Cannot archive root user.')
|
|
1324
|
+
|
|
1325
|
+
await self.auth.setUserInfo(self.iden, 'archived', archived)
|
|
1326
|
+
if archived:
|
|
1327
|
+
await self.setLocked(True)
|
|
1328
|
+
|
|
1329
|
+
async def tryPasswd(self, passwd, nexs=True, enforce_policy=True):
|
|
1330
|
+
|
|
1331
|
+
if self.isLocked():
|
|
1332
|
+
return False
|
|
1333
|
+
|
|
1334
|
+
if passwd is None:
|
|
1335
|
+
return False
|
|
1336
|
+
|
|
1337
|
+
onepass = self.info.get('onepass')
|
|
1338
|
+
if onepass is not None:
|
|
1339
|
+
if isinstance(onepass, dict):
|
|
1340
|
+
shadow = onepass.get('shadow')
|
|
1341
|
+
expires = onepass.get('expires')
|
|
1342
|
+
if expires >= s_common.now():
|
|
1343
|
+
if await s_passwd.checkShadowV2(passwd=passwd, shadow=shadow):
|
|
1344
|
+
await self.auth.setUserInfo(self.iden, 'onepass', None)
|
|
1345
|
+
logger.debug(f'Used one time password for {self.name}',
|
|
1346
|
+
extra={'synapse': {'user': self.iden, 'username': self.name}})
|
|
1347
|
+
return True
|
|
1348
|
+
else:
|
|
1349
|
+
# Backwards compatible password handling
|
|
1350
|
+
expires, params, hashed = onepass
|
|
1351
|
+
if expires >= s_common.now():
|
|
1352
|
+
if s_common.guid((params, passwd)) == hashed:
|
|
1353
|
+
await self.auth.setUserInfo(self.iden, 'onepass', None)
|
|
1354
|
+
logger.debug(f'Used one time password for {self.name}',
|
|
1355
|
+
extra={'synapse': {'user': self.iden, 'username': self.name}})
|
|
1356
|
+
return True
|
|
1357
|
+
|
|
1358
|
+
shadow = self.info.get('passwd')
|
|
1359
|
+
if shadow is None:
|
|
1360
|
+
return False
|
|
1361
|
+
|
|
1362
|
+
if isinstance(shadow, dict):
|
|
1363
|
+
result = await s_passwd.checkShadowV2(passwd=passwd, shadow=shadow)
|
|
1364
|
+
if self.auth.policy and (attempts := self.auth.policy.get('attempts')) is not None:
|
|
1365
|
+
valu = self.info.get('policy:attempts', 0)
|
|
1366
|
+
if result:
|
|
1367
|
+
if valu > 0:
|
|
1368
|
+
await self.auth.setUserInfo(self.iden, 'policy:attempts', 0)
|
|
1369
|
+
return True
|
|
1370
|
+
|
|
1371
|
+
if enforce_policy:
|
|
1372
|
+
|
|
1373
|
+
valu += 1
|
|
1374
|
+
await self.auth.setUserInfo(self.iden, 'policy:attempts', valu)
|
|
1375
|
+
|
|
1376
|
+
if valu >= attempts:
|
|
1377
|
+
|
|
1378
|
+
if self.iden == self.auth.rootuser.iden:
|
|
1379
|
+
mesg = f'User {self.name} has exceeded the number of allowed password attempts ({valu + 1}),. Cannot lock {self.name} user.'
|
|
1380
|
+
extra = {'synapse': {'target_user': self.iden, 'target_username': self.name, }}
|
|
1381
|
+
logger.error(mesg, extra=extra)
|
|
1382
|
+
return False
|
|
1383
|
+
|
|
1384
|
+
await self.auth.nexsroot.cell.setUserLocked(self.iden, True)
|
|
1385
|
+
|
|
1386
|
+
mesg = f'User {self.name} has exceeded the number of allowed password attempts ({valu + 1}), locking their account.'
|
|
1387
|
+
extra = {'synapse': {'target_user': self.iden, 'target_username': self.name, 'status': 'MODIFY'}}
|
|
1388
|
+
logger.warning(mesg, extra=extra)
|
|
1389
|
+
|
|
1390
|
+
return False
|
|
1391
|
+
|
|
1392
|
+
return result
|
|
1393
|
+
|
|
1394
|
+
# Backwards compatible password handling
|
|
1395
|
+
salt, hashed = shadow
|
|
1396
|
+
if s_common.guid((salt, passwd)) == hashed:
|
|
1397
|
+
logger.debug(f'Migrating password to shadowv2 format for user {self.name}',
|
|
1398
|
+
extra={'synapse': {'user': self.iden, 'username': self.name}})
|
|
1399
|
+
# Update user to new password hashing scheme. We cannot enforce policy
|
|
1400
|
+
# when migrating an existing password.
|
|
1401
|
+
await self.setPasswd(passwd=passwd, nexs=nexs, enforce_policy=False)
|
|
1402
|
+
|
|
1403
|
+
return True
|
|
1404
|
+
|
|
1405
|
+
return False
|
|
1406
|
+
|
|
1407
|
+
async def _checkPasswdPolicy(self, passwd, shadow, nexs=True):
|
|
1408
|
+
if not self.auth.policy:
|
|
1409
|
+
return
|
|
1410
|
+
|
|
1411
|
+
failures = []
|
|
1412
|
+
|
|
1413
|
+
# Check complexity of password
|
|
1414
|
+
complexity = self.auth.policy.get('complexity')
|
|
1415
|
+
if complexity is not None:
|
|
1416
|
+
|
|
1417
|
+
# Check password length
|
|
1418
|
+
minlen = complexity.get('length')
|
|
1419
|
+
if minlen is not None and (passwd is None or len(passwd) < minlen):
|
|
1420
|
+
failures.append(f'Password must be at least {minlen} characters.')
|
|
1421
|
+
|
|
1422
|
+
if minlen is not None and passwd is None:
|
|
1423
|
+
# Set password to empty string so we get the rest of the failure info
|
|
1424
|
+
passwd = ''
|
|
1425
|
+
|
|
1426
|
+
if passwd is None:
|
|
1427
|
+
return
|
|
1428
|
+
|
|
1429
|
+
allvalid = []
|
|
1430
|
+
|
|
1431
|
+
# Check uppercase
|
|
1432
|
+
count = complexity.get('upper:count', 0)
|
|
1433
|
+
if (valid := complexity.get('upper:valid', string.ascii_uppercase)):
|
|
1434
|
+
allvalid.append(valid)
|
|
1435
|
+
if count is not None and (found := len([k for k in passwd if k in valid])) < count:
|
|
1436
|
+
failures.append(f'Password must contain at least {count} uppercase characters, {found} found.')
|
|
1437
|
+
|
|
1438
|
+
# Check lowercase
|
|
1439
|
+
count = complexity.get('lower:count', 0)
|
|
1440
|
+
if (valid := complexity.get('lower:valid', string.ascii_lowercase)):
|
|
1441
|
+
allvalid.append(valid)
|
|
1442
|
+
|
|
1443
|
+
if count is not None and (found := len([k for k in passwd if k in valid])) < count:
|
|
1444
|
+
failures.append(f'Password must contain at least {count} lowercase characters, {found} found.')
|
|
1445
|
+
|
|
1446
|
+
# Check special
|
|
1447
|
+
count = complexity.get('special:count', 0)
|
|
1448
|
+
if (valid := complexity.get('special:valid', string.punctuation)):
|
|
1449
|
+
allvalid.append(valid)
|
|
1450
|
+
|
|
1451
|
+
if count is not None and (found := len([k for k in passwd if k in valid])) < count:
|
|
1452
|
+
failures.append(f'Password must contain at least {count} special characters, {found} found.')
|
|
1453
|
+
|
|
1454
|
+
# Check numbers
|
|
1455
|
+
count = complexity.get('number:count', 0)
|
|
1456
|
+
if (valid := complexity.get('number:valid', string.digits)):
|
|
1457
|
+
allvalid.append(valid)
|
|
1458
|
+
if count is not None and (found := len([k for k in passwd if k in valid])) < count:
|
|
1459
|
+
failures.append(f'Password must contain at least {count} digit characters, {found} found.')
|
|
1460
|
+
|
|
1461
|
+
if allvalid:
|
|
1462
|
+
allvalid = ''.join(allvalid)
|
|
1463
|
+
if (invalid := set(passwd) - set(allvalid)):
|
|
1464
|
+
failures.append(f'Password contains invalid characters: {sorted(list(invalid))}')
|
|
1465
|
+
|
|
1466
|
+
# Check sequences
|
|
1467
|
+
seqlen = complexity.get('sequences')
|
|
1468
|
+
if seqlen is not None:
|
|
1469
|
+
# Convert each character to it's ordinal value so we can look for
|
|
1470
|
+
# forward and reverse sequences in windows of seqlen. Doing it this
|
|
1471
|
+
# way allows us to easily check unicode sequences too.
|
|
1472
|
+
passb = [ord(k) for k in passwd]
|
|
1473
|
+
for offs in range(len(passwd) - (seqlen - 1)):
|
|
1474
|
+
curv = passb[offs]
|
|
1475
|
+
fseq = list(range(curv, curv + seqlen))
|
|
1476
|
+
rseq = list(range(curv, curv - seqlen, -1))
|
|
1477
|
+
window = passb[offs:offs + seqlen]
|
|
1478
|
+
if window == fseq or window == rseq:
|
|
1479
|
+
failures.append(f'Password must not contain forward/reverse sequences longer than {seqlen} characters.')
|
|
1480
|
+
break
|
|
1481
|
+
|
|
1482
|
+
# Check for previous password reuse
|
|
1483
|
+
prevvalu = self.auth.policy.get('previous')
|
|
1484
|
+
if prevvalu is not None:
|
|
1485
|
+
previous = self.info.get('policy:previous', ())
|
|
1486
|
+
for prevshad in previous:
|
|
1487
|
+
if await s_passwd.checkShadowV2(passwd, prevshad):
|
|
1488
|
+
failures.append(f'Password cannot be the same as previous {prevvalu} password(s).')
|
|
1489
|
+
break
|
|
1490
|
+
|
|
1491
|
+
if failures:
|
|
1492
|
+
mesg = ['Cannot change password due to the following policy violations:']
|
|
1493
|
+
mesg.extend(f' - {msg}' for msg in failures)
|
|
1494
|
+
raise s_exc.BadArg(mesg='\n'.join(mesg), failures=failures)
|
|
1495
|
+
|
|
1496
|
+
if prevvalu is not None:
|
|
1497
|
+
# Looks like this password is good, add it to the list of previous passwords
|
|
1498
|
+
previous = self.info.get('policy:previous', ())
|
|
1499
|
+
previous = (shadow,) + previous
|
|
1500
|
+
if nexs:
|
|
1501
|
+
await self.auth.setUserInfo(self.iden, 'policy:previous', previous[:prevvalu])
|
|
1502
|
+
else:
|
|
1503
|
+
await self.auth._hndlsetUserInfo(self.iden, 'policy:previous', previous[:prevvalu], logged=nexs)
|
|
1504
|
+
|
|
1505
|
+
async def setPasswd(self, passwd, nexs=True, enforce_policy=True):
|
|
1506
|
+
# Prevent empty string or non-string values
|
|
1507
|
+
if passwd is None:
|
|
1508
|
+
shadow = None
|
|
1509
|
+
elif passwd and isinstance(passwd, str):
|
|
1510
|
+
shadow = await s_passwd.getShadowV2(passwd=passwd)
|
|
1511
|
+
else:
|
|
1512
|
+
raise s_exc.BadArg(mesg='Password must be a string')
|
|
1513
|
+
|
|
1514
|
+
if enforce_policy:
|
|
1515
|
+
await self._checkPasswdPolicy(passwd, shadow, nexs=nexs)
|
|
1516
|
+
|
|
1517
|
+
if nexs:
|
|
1518
|
+
await self.auth.setUserInfo(self.iden, 'passwd', shadow)
|
|
1519
|
+
else:
|
|
1520
|
+
await self.auth._hndlsetUserInfo(self.iden, 'passwd', shadow, logged=nexs)
|