synapse 2.175.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.

Files changed (73) hide show
  1. synapse/axon.py +24 -9
  2. synapse/cortex.py +330 -168
  3. synapse/cryotank.py +46 -37
  4. synapse/datamodel.py +17 -4
  5. synapse/exc.py +19 -0
  6. synapse/lib/agenda.py +7 -13
  7. synapse/lib/ast.py +6 -5
  8. synapse/lib/auth.py +1520 -0
  9. synapse/lib/cell.py +255 -53
  10. synapse/lib/grammar.py +5 -0
  11. synapse/lib/hive.py +24 -3
  12. synapse/lib/hiveauth.py +6 -32
  13. synapse/lib/layer.py +7 -4
  14. synapse/lib/link.py +21 -17
  15. synapse/lib/lmdbslab.py +149 -0
  16. synapse/lib/modelrev.py +1 -1
  17. synapse/lib/schemas.py +136 -0
  18. synapse/lib/storm.py +70 -33
  19. synapse/lib/stormlib/aha.py +1 -1
  20. synapse/lib/stormlib/auth.py +185 -10
  21. synapse/lib/stormlib/cortex.py +16 -5
  22. synapse/lib/stormlib/gen.py +80 -0
  23. synapse/lib/stormlib/model.py +55 -0
  24. synapse/lib/stormlib/modelext.py +60 -0
  25. synapse/lib/stormlib/storm.py +117 -5
  26. synapse/lib/stormlib/tabular.py +212 -0
  27. synapse/lib/stormtypes.py +14 -1
  28. synapse/lib/trigger.py +1 -1
  29. synapse/lib/version.py +2 -2
  30. synapse/lib/view.py +55 -28
  31. synapse/models/base.py +7 -0
  32. synapse/models/biz.py +4 -0
  33. synapse/models/files.py +8 -1
  34. synapse/models/inet.py +31 -0
  35. synapse/tests/files/changelog/model_2.176.0_16ee721a6b7221344eaf946c3ab4602dda546b1a.yaml.gz +0 -0
  36. synapse/tests/files/changelog/model_2.176.0_2a25c58bbd344716cd7cbc3f4304d8925b0f4ef2.yaml.gz +0 -0
  37. synapse/tests/test_axon.py +7 -4
  38. synapse/tests/test_cortex.py +127 -82
  39. synapse/tests/test_cryotank.py +4 -4
  40. synapse/tests/test_datamodel.py +7 -0
  41. synapse/tests/test_lib_agenda.py +7 -0
  42. synapse/tests/{test_lib_hiveauth.py → test_lib_auth.py} +314 -11
  43. synapse/tests/test_lib_cell.py +161 -8
  44. synapse/tests/test_lib_httpapi.py +18 -14
  45. synapse/tests/test_lib_layer.py +33 -33
  46. synapse/tests/test_lib_link.py +42 -1
  47. synapse/tests/test_lib_lmdbslab.py +68 -0
  48. synapse/tests/test_lib_nexus.py +4 -4
  49. synapse/tests/test_lib_node.py +0 -7
  50. synapse/tests/test_lib_storm.py +45 -0
  51. synapse/tests/test_lib_stormlib_aha.py +1 -2
  52. synapse/tests/test_lib_stormlib_auth.py +21 -0
  53. synapse/tests/test_lib_stormlib_cortex.py +12 -12
  54. synapse/tests/test_lib_stormlib_gen.py +99 -0
  55. synapse/tests/test_lib_stormlib_model.py +108 -0
  56. synapse/tests/test_lib_stormlib_modelext.py +64 -0
  57. synapse/tests/test_lib_stormlib_storm.py +82 -1
  58. synapse/tests/test_lib_stormlib_tabular.py +226 -0
  59. synapse/tests/test_lib_stormsvc.py +4 -1
  60. synapse/tests/test_lib_stormtypes.py +10 -0
  61. synapse/tests/test_model_base.py +3 -0
  62. synapse/tests/test_model_biz.py +3 -0
  63. synapse/tests/test_model_files.py +12 -2
  64. synapse/tests/test_model_inet.py +55 -0
  65. synapse/tests/test_tools_changelog.py +196 -0
  66. synapse/tests/test_tools_healthcheck.py +4 -3
  67. synapse/tests/utils.py +1 -1
  68. synapse/tools/changelog.py +774 -15
  69. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/METADATA +3 -3
  70. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/RECORD +73 -67
  71. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/WHEEL +1 -1
  72. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/LICENSE +0 -0
  73. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/top_level.txt +0 -0
synapse/lib/hiveauth.py CHANGED
@@ -277,7 +277,6 @@ class Auth(s_nexus.Pusher):
277
277
 
278
278
  return user
279
279
 
280
- @s_nexus.Pusher.onPushAuto('user:name')
281
280
  async def setUserName(self, iden, name):
282
281
  if not isinstance(name, str):
283
282
  raise s_exc.BadArg(mesg='setUserName() name must be a string')
@@ -302,7 +301,6 @@ class Auth(s_nexus.Pusher):
302
301
  }
303
302
  await self.feedBeholder('user:name', beheld)
304
303
 
305
- @s_nexus.Pusher.onPushAuto('role:name')
306
304
  async def setRoleName(self, iden, name):
307
305
  if not isinstance(name, str):
308
306
  raise s_exc.BadArg(mesg='setRoleName() name must be a string')
@@ -346,7 +344,6 @@ class Auth(s_nexus.Pusher):
346
344
 
347
345
  await self.fire('cell:beholder', **behold)
348
346
 
349
- @s_nexus.Pusher.onPushAuto('user:info')
350
347
  async def setUserInfo(self, iden, name, valu, gateiden=None, logged=True, mesg=None):
351
348
 
352
349
  user = await self.reqUser(iden)
@@ -373,7 +370,6 @@ class Auth(s_nexus.Pusher):
373
370
  # since any user info *may* effect auth
374
371
  user.clearAuthCache()
375
372
 
376
- @s_nexus.Pusher.onPushAuto('role:info')
377
373
  async def setRoleInfo(self, iden, name, valu, gateiden=None, logged=True, mesg=None):
378
374
  role = await self.reqRole(iden)
379
375
 
@@ -528,7 +524,6 @@ class Auth(s_nexus.Pusher):
528
524
 
529
525
  return user
530
526
 
531
- @s_nexus.Pusher.onPush('user:add')
532
527
  async def _addUser(self, iden, name):
533
528
 
534
529
  user = self.usersbyname.get(name)
@@ -554,7 +549,6 @@ class Auth(s_nexus.Pusher):
554
549
 
555
550
  return self.role(iden)
556
551
 
557
- @s_nexus.Pusher.onPush('role:add')
558
552
  async def _addRole(self, iden, name):
559
553
 
560
554
  role = self.rolesbyname.get(name)
@@ -574,7 +568,6 @@ class Auth(s_nexus.Pusher):
574
568
  await self.reqUser(iden)
575
569
  return await self._push('user:del', iden)
576
570
 
577
- @s_nexus.Pusher.onPush('user:del')
578
571
  async def _delUser(self, iden):
579
572
 
580
573
  if iden == self.rootuser.iden:
@@ -608,7 +601,6 @@ class Auth(s_nexus.Pusher):
608
601
  await self.reqRole(iden)
609
602
  return await self._push('role:del', iden)
610
603
 
611
- @s_nexus.Pusher.onPush('role:del')
612
604
  async def _delRole(self, iden):
613
605
 
614
606
  if iden == self.allrole.iden:
@@ -838,10 +830,7 @@ class HiveRole(HiveRuler):
838
830
  }
839
831
 
840
832
  async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):
841
- if nexs:
842
- return await self.auth.setRoleInfo(self.iden, name, valu, gateiden=gateiden, mesg=mesg)
843
- else:
844
- return await self.auth._hndlsetRoleInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
833
+ return await self.auth.setRoleInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
845
834
 
846
835
  async def setName(self, name):
847
836
  return await self.auth.setRoleName(self.iden, name)
@@ -929,10 +918,7 @@ class HiveUser(HiveRuler):
929
918
  }
930
919
 
931
920
  async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):
932
- if nexs:
933
- return await self.auth.setUserInfo(self.iden, name, valu, gateiden=gateiden, mesg=mesg)
934
- else:
935
- return await self.auth._hndlsetUserInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
921
+ return await self.auth.setUserInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
936
922
 
937
923
  async def setName(self, name):
938
924
  return await self.auth.setUserName(self.iden, name)
@@ -1232,10 +1218,7 @@ class HiveUser(HiveRuler):
1232
1218
 
1233
1219
  roles.remove(role.iden)
1234
1220
  mesg = {'name': 'role:revoke', 'iden': self.iden, 'role': role.pack()}
1235
- if nexs:
1236
- await self.auth.setUserInfo(self.iden, 'roles', roles, mesg=mesg)
1237
- else:
1238
- await self.auth._hndlsetUserInfo(self.iden, 'roles', roles, logged=nexs, mesg=mesg)
1221
+ await self.auth.setUserInfo(self.iden, 'roles', roles, logged=nexs, mesg=mesg)
1239
1222
 
1240
1223
  def isLocked(self):
1241
1224
  return self.info.get('locked')
@@ -1271,18 +1254,12 @@ class HiveUser(HiveRuler):
1271
1254
  async def setAdmin(self, admin, gateiden=None, logged=True):
1272
1255
  if not isinstance(admin, bool):
1273
1256
  raise s_exc.BadArg(mesg='setAdmin requires a boolean')
1274
- if logged:
1275
- await self.auth.setUserInfo(self.iden, 'admin', admin, gateiden=gateiden)
1276
- else:
1277
- await self.auth._hndlsetUserInfo(self.iden, 'admin', admin, gateiden=gateiden, logged=logged)
1257
+ await self.auth.setUserInfo(self.iden, 'admin', admin, gateiden=gateiden, logged=logged)
1278
1258
 
1279
1259
  async def setLocked(self, locked, logged=True):
1280
1260
  if not isinstance(locked, bool):
1281
1261
  raise s_exc.BadArg(mesg='setLocked requires a boolean')
1282
- if logged:
1283
- await self.auth.setUserInfo(self.iden, 'locked', locked)
1284
- else:
1285
- await self.auth._hndlsetUserInfo(self.iden, 'locked', locked, logged=logged)
1262
+ await self.auth.setUserInfo(self.iden, 'locked', locked, logged=logged)
1286
1263
 
1287
1264
  async def setArchived(self, archived):
1288
1265
  if not isinstance(archived, bool):
@@ -1347,7 +1324,4 @@ class HiveUser(HiveRuler):
1347
1324
  shadow = await s_passwd.getShadowV2(passwd=passwd)
1348
1325
  else:
1349
1326
  raise s_exc.BadArg(mesg='Password must be a string')
1350
- if nexs:
1351
- await self.auth.setUserInfo(self.iden, 'passwd', shadow)
1352
- else:
1353
- await self.auth._hndlsetUserInfo(self.iden, 'passwd', shadow, logged=nexs)
1327
+ await self.auth.setUserInfo(self.iden, 'passwd', shadow, logged=nexs)
synapse/lib/layer.py CHANGED
@@ -2034,7 +2034,7 @@ class Layer(s_nexus.Pusher):
2034
2034
  'stortype': stortype})
2035
2035
 
2036
2036
  async def pack(self):
2037
- ret = self.layrinfo.pack()
2037
+ ret = deepcopy(self.layrinfo)
2038
2038
  if ret.get('mirror'):
2039
2039
  ret['mirror'] = s_urlhelp.sanitizeUrl(ret['mirror'])
2040
2040
  ret['offset'] = await self.getEditIndx()
@@ -2810,9 +2810,11 @@ class Layer(s_nexus.Pusher):
2810
2810
 
2811
2811
  # TODO when we can set more props, we may need to parse values.
2812
2812
  if valu is None:
2813
- await self.layrinfo.pop(name)
2813
+ self.layrinfo.pop(name, None)
2814
2814
  else:
2815
- await self.layrinfo.set(name, valu)
2815
+ self.layrinfo[name] = valu
2816
+
2817
+ self.core.layerdefs.set(self.iden, self.layrinfo)
2816
2818
 
2817
2819
  await self.core.feedBeholder('layer:set', {'iden': self.iden, 'name': name, 'valu': valu}, gates=[self.iden])
2818
2820
  return valu
@@ -4453,7 +4455,8 @@ class Layer(s_nexus.Pusher):
4453
4455
 
4454
4456
  @s_nexus.Pusher.onPush('layer:set:modelvers')
4455
4457
  async def _setModelVers(self, vers):
4456
- await self.layrinfo.set('model:version', vers)
4458
+ self.layrinfo['model:version'] = vers
4459
+ self.core.layerdefs.set(self.iden, self.layrinfo)
4457
4460
 
4458
4461
  async def getStorNodes(self):
4459
4462
  '''
synapse/lib/link.py CHANGED
@@ -21,7 +21,7 @@ async def connect(host, port, ssl=None, hostname=None, linkinfo=None):
21
21
  '''
22
22
  Async connect and return a Link().
23
23
  '''
24
- info = {'host': host, 'port': port, 'ssl': ssl, 'hostname': hostname}
24
+ info = {'host': host, 'port': port, 'ssl': ssl, 'hostname': hostname, 'tls': bool(ssl)}
25
25
  if linkinfo is not None:
26
26
  info.update(linkinfo)
27
27
 
@@ -137,6 +137,25 @@ class Link(s_base.Base):
137
137
 
138
138
  self.info = info
139
139
 
140
+ self._addrinfo = {}
141
+ # _addrinfo is populated in this order so that as first hit tls links (prod deployments)
142
+ # then unix links (unit tests with local sockets, container healthchecks, local tools )
143
+ # then tcp links ( unit tests and legacy deployments )
144
+ if self.info.get('tls'):
145
+ self._addrinfo['family'] = 'tls'
146
+ self._addrinfo['addr'] = self.sock.getpeername()
147
+ elif self.info.get('unix'):
148
+ self._addrinfo['family'] = 'unix'
149
+ # Unix sockets don't use getpeername
150
+ self._addrinfo['addr'] = self.sock.getsockname()
151
+ else:
152
+ self._addrinfo['family'] = 'tcp'
153
+ self._addrinfo['addr'] = self.sock.getpeername()
154
+ if self.sock.family == socket.AF_INET:
155
+ self._addrinfo['ipver'] = 'ipv4'
156
+ elif self.sock.family == socket.AF_INET6:
157
+ self._addrinfo['ipver'] = 'ipv6'
158
+
140
159
  self.unpk = s_msgpack.Unpk()
141
160
 
142
161
  async def fini():
@@ -212,22 +231,7 @@ class Link(s_base.Base):
212
231
  '''
213
232
  Get a summary of address information related to the link.
214
233
  '''
215
- ret = {'family': 'tcp',
216
- 'addr': self.sock.getpeername(),
217
- }
218
- # Set family information
219
- if self.info.get('unix'):
220
- ret['family'] = 'unix'
221
- # Unix sockets don't use getpeername
222
- ret['addr'] = self.sock.getsockname()
223
- elif self.info.get('tls'):
224
- ret['family'] = 'tls'
225
- # Set ipver if needed
226
- if self.sock.family == socket.AF_INET:
227
- ret['ipver'] = 'ipv4'
228
- if self.sock.family == socket.AF_INET6:
229
- ret['ipver'] = 'ipv6'
230
- return ret
234
+ return dict(self._addrinfo)
231
235
 
232
236
  async def send(self, byts):
233
237
  self.writer.write(byts)
synapse/lib/lmdbslab.py CHANGED
@@ -166,6 +166,148 @@ class SlabDict:
166
166
  self.set(name, curv)
167
167
  return curv
168
168
 
169
+ class SafeKeyVal:
170
+ '''
171
+ Key/value storage that does not store items in memory and ensures keys are < 512 characters in length.
172
+
173
+ Note:
174
+ The key size limitation includes the length of any prefixes added by
175
+ using getSubKeyVal().
176
+ '''
177
+ def __init__(self, slab, name, prefix=''):
178
+
179
+ self.prefix = prefix
180
+ self._prefix = prefix.encode('utf8')
181
+ self.preflen = len(self._prefix)
182
+
183
+ if self.preflen > 510:
184
+ mesg = 'SafeKeyVal prefix lengths must be < 511 bytes.'
185
+ raise s_exc.BadArg(mesg, prefix=self._prefix[:1024])
186
+
187
+ self.name = name
188
+ self.slab = slab
189
+ self.valudb = slab.initdb(name)
190
+
191
+ def getSubKeyVal(self, prefix):
192
+
193
+ if not prefix or not isinstance(prefix, str):
194
+ mesg = 'SafeKeyVal.getSubKeyVal() requires a string prefix of at least one character.'
195
+ raise s_exc.BadArg(mesg, prefix=prefix)
196
+
197
+ if self.prefix:
198
+ prefix = self.prefix + prefix
199
+
200
+ return SafeKeyVal(self.slab, self.name, prefix=prefix)
201
+
202
+ def reqValidName(self, name):
203
+
204
+ _name = name.encode('utf-8')
205
+
206
+ if self._prefix:
207
+ _name = self._prefix + _name
208
+
209
+ if len(_name) > 511:
210
+ maxlen = 511 - self.preflen
211
+ mesg = f'SafeKeyVal keys with prefix {self.prefix} must be less < {maxlen} bytes in length.'
212
+ raise s_exc.BadArg(mesg, prefix=self.prefix, name=name[:1024])
213
+ return _name
214
+
215
+ def get(self, name, defv=None):
216
+ '''
217
+ Get the value for a key.
218
+
219
+ Note:
220
+ This may only be used for keys < 512 characters in length.
221
+ '''
222
+ name = self.reqValidName(name)
223
+
224
+ if (byts := self.slab.get(name, db=self.valudb)) is None:
225
+ return defv
226
+ return s_msgpack.un(byts)
227
+
228
+ def set(self, name, valu):
229
+
230
+ name = self.reqValidName(name)
231
+
232
+ self.slab.put(name, s_msgpack.en(valu), db=self.valudb)
233
+ return valu
234
+
235
+ def pop(self, name, defv=None):
236
+
237
+ name = self.reqValidName(name)
238
+
239
+ if (byts := self.slab.pop(name, db=self.valudb)) is not None:
240
+ return s_msgpack.un(byts)
241
+ return defv
242
+
243
+ def delete(self, name):
244
+ '''
245
+ Delete a key.
246
+ '''
247
+ name = self.reqValidName(name)
248
+ return self.slab.delete(name, db=self.valudb)
249
+
250
+ async def truncate(self, pref=''):
251
+ '''
252
+ Delete all keys.
253
+ '''
254
+ pref = self.reqValidName(pref)
255
+
256
+ if not pref:
257
+ genr = self.slab.scanKeys(db=self.valudb)
258
+ else:
259
+ genr = self.slab.scanKeysByPref(pref, db=self.valudb)
260
+
261
+ for lkey in genr:
262
+ self.slab.delete(lkey, db=self.valudb)
263
+ await asyncio.sleep(0)
264
+
265
+ def items(self, pref=''):
266
+
267
+ pref = self.reqValidName(pref)
268
+
269
+ if not pref:
270
+ genr = self.slab.scanByFull(db=self.valudb)
271
+ else:
272
+ genr = self.slab.scanByPref(pref, db=self.valudb)
273
+
274
+ if self.prefix:
275
+ for lkey, byts in genr:
276
+ yield lkey[self.preflen:].decode('utf8'), s_msgpack.un(byts)
277
+ return
278
+
279
+ for lkey, byts in genr:
280
+ yield lkey.decode('utf8'), s_msgpack.un(byts)
281
+
282
+ def keys(self, pref=''):
283
+
284
+ pref = self.reqValidName(pref)
285
+
286
+ if not pref:
287
+ genr = self.slab.scanKeys(db=self.valudb)
288
+ else:
289
+ genr = self.slab.scanKeysByPref(pref, db=self.valudb)
290
+
291
+ if self.prefix:
292
+ for lkey in genr:
293
+ yield lkey[self.preflen:].decode('utf8')
294
+ return
295
+
296
+ for lkey in genr:
297
+ yield lkey.decode('utf8')
298
+
299
+ def values(self, pref=''):
300
+
301
+ pref = self.reqValidName(pref)
302
+
303
+ if not pref:
304
+ genr = self.slab.scanByFull(db=self.valudb)
305
+ else:
306
+ genr = self.slab.scanByPref(pref, db=self.valudb)
307
+
308
+ for lkey, byts in genr:
309
+ yield s_msgpack.un(byts)
310
+
169
311
  class SlabAbrv:
170
312
  '''
171
313
  A utility for translating arbitrary bytes into fixed with id bytes
@@ -845,6 +987,13 @@ class Slab(s_base.Base):
845
987
  self.onfini(mq)
846
988
  return mq
847
989
 
990
+ def getSafeKeyVal(self, name, prefix='', create=True):
991
+ if not create and not self.dbexists(name):
992
+ mesg = f'Database {name=} does not exist.'
993
+ raise s_exc.BadArg(mesg=mesg)
994
+
995
+ return SafeKeyVal(self, name, prefix=prefix)
996
+
848
997
  def statinfo(self):
849
998
  return {
850
999
  'locking_memory': self.locking_memory, # whether the memory lock loop was started and hasn't ended
synapse/lib/modelrev.py CHANGED
@@ -821,7 +821,7 @@ class ModelRev:
821
821
  # that we are not able to rev ourselves and bail...
822
822
 
823
823
  layers = []
824
- for layr in self.core.layers.values():
824
+ for layr in list(self.core.layers.values()):
825
825
 
826
826
  if layr.fresh:
827
827
  await layr.setModelVers(version)
synapse/lib/schemas.py CHANGED
@@ -288,6 +288,93 @@ _stormPoolOptsSchema = {
288
288
  }
289
289
  reqValidStormPoolOpts = s_config.getJsValidator(_stormPoolOptsSchema)
290
290
 
291
+ _authRulesSchema = {
292
+ 'type': 'array',
293
+ 'items': {
294
+ 'type': 'array',
295
+ 'items': [
296
+ {'type': 'boolean'},
297
+ {'type': 'array', 'items': {'type': 'string'}},
298
+ ],
299
+ 'minItems': 2,
300
+ 'maxItems': 2,
301
+ }
302
+ }
303
+ reqValidRules = s_config.getJsValidator(_authRulesSchema)
304
+
305
+ _passwdPolicySchema = {
306
+ 'type': 'object',
307
+ 'properties': {
308
+ 'complexity': {
309
+ 'type': ['object', 'null'],
310
+ 'properties': {
311
+ 'length': {
312
+ 'type': ['number', 'null'],
313
+ 'minimum': 1,
314
+ 'description': 'Minimum password character length.',
315
+ },
316
+ 'sequences': {
317
+ 'type': ['number', 'null'],
318
+ 'minimum': 2,
319
+ 'description': 'Maximum sequence length in a password. Sequences can be letters or number, forward or reverse.',
320
+ },
321
+ 'upper:count': {
322
+ 'type': ['number', 'null'],
323
+ 'description': 'The minimum number of uppercase characters required in password.',
324
+ },
325
+ 'upper:valid': {
326
+ 'type': ['string', 'null'],
327
+ 'minLength': 1,
328
+ 'description': 'All valid uppercase characters.',
329
+ },
330
+ 'lower:count': {
331
+ 'type': ['number', 'null'],
332
+ 'minimum': 0,
333
+ 'description': 'The minimum number of lowercase characters required in password.',
334
+ },
335
+ 'lower:valid': {
336
+ 'type': ['string', 'null'],
337
+ 'minLength': 1,
338
+ 'description': 'All valid lowercase characters.',
339
+ },
340
+ 'special:count': {
341
+ 'type': ['number', 'null'],
342
+ 'minimum': 0,
343
+ 'description': 'The minimum number of special characters required in password.',
344
+ },
345
+ 'special:valid': {
346
+ 'type': ['string', 'null'],
347
+ 'minLength': 1,
348
+ 'description': 'All valid special characters.',
349
+ },
350
+ 'number:count': {
351
+ 'type': ['number', 'null'],
352
+ 'minimum': 0,
353
+ 'description': 'The minimum number of digit characters required in password.',
354
+ },
355
+ 'number:valid': {
356
+ 'type': ['string', 'null'],
357
+ 'minLength': 1,
358
+ 'description': 'All valid digit characters.',
359
+ },
360
+ },
361
+ 'additionalProperties': False,
362
+ },
363
+ 'attempts': {
364
+ 'type': ['number', 'null'],
365
+ 'minimum': 1,
366
+ 'description': 'Maximum number of incorrect attempts before locking user account.',
367
+ },
368
+ 'previous': {
369
+ 'type': ['number', 'null'],
370
+ 'minimum': 1,
371
+ 'description': 'Number of previous passwords to disallow.',
372
+ },
373
+ },
374
+ 'additionalProperties': False,
375
+ }
376
+ reqValidPasswdPolicy = s_config.getJsValidator(_passwdPolicySchema)
377
+
291
378
  # These types are order sensitive
292
379
  _changelogTypes = {'migration': 'Automatic Migrations',
293
380
  'model': 'Model Changes',
@@ -318,3 +405,52 @@ _changelogSchema = {
318
405
  'required': ['type', 'desc']
319
406
  }
320
407
  _reqChanglogSchema = s_config.getJsValidator(_changelogSchema)
408
+
409
+ tabularConfSchema = {
410
+ 'type': 'object',
411
+ 'properties': {
412
+ 'separators': {
413
+ 'type': 'object',
414
+ 'properties': {
415
+ 'row:outline': {'type': 'boolean', 'default': False,
416
+ 'description': 'Add the row separator before the header data and after each row.'},
417
+ 'column:outline': {'type': 'boolean', 'default': False,
418
+ 'description': 'Add the column separator to the beginning and end of each row.'},
419
+ 'header:row': {'type': 'string', 'default': '=',
420
+ 'description': 'The string to use to create a separator row when printing the header.'},
421
+ 'data:row': {'type': 'string', 'default': '-',
422
+ 'description': 'The string to use to create a separator row when printing data rows.'},
423
+ 'column': {'type': 'string', 'default': '|',
424
+ 'description': 'The string to use to separate columns.'},
425
+ },
426
+ 'additionalProperties': False,
427
+ },
428
+ 'columns': {
429
+ 'type': 'array',
430
+ 'items': {
431
+ 'type': 'object',
432
+ 'properties': {
433
+ 'name': {'type': 'string',
434
+ 'description': 'The column name which will be used in the header row.'},
435
+ 'width': {'type': 'number', 'default': None, 'exclusiveMinimum': 0,
436
+ 'description': 'If not provided each cell will expand to fit the data.'},
437
+ 'justify': {'type': 'string', 'default': 'left', 'enum': ['left', 'center', 'right'],
438
+ 'description': 'Justification for the header titles and data rows.'},
439
+ 'overflow': {'type': 'string', 'default': 'trim', 'enum': ['wrap', 'trim'],
440
+ 'description': 'For text exceeding the width, '
441
+ 'either wrap text in multiple lines or trim and append "...".'},
442
+ 'newlines': {'type': 'string', 'default': 'replace', 'enum': ['replace', 'split'],
443
+ 'description': 'Replace newlines with a space or split into multiple lines.'
444
+ 'Split is only applied if width is undefined.'},
445
+ },
446
+ 'required': ['name'],
447
+ 'minItems': 1,
448
+ 'additionalProperties': False,
449
+ },
450
+ },
451
+ },
452
+ 'required': ['columns'],
453
+ 'additionalProperties': False,
454
+ }
455
+
456
+ reqValidTabularConf = s_config.getJsValidator(tabularConfSchema)