-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathFlaskSimpleAuth.py
executable file
·4682 lines (3866 loc) · 183 KB
/
FlaskSimpleAuth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Flask Extension and Wrapper
This extension helps manage:
- authentication
- authorization
- parameters
- and more…
This code is public domain.
"""
# TODO refactoring
# - clarify manager public/private interfaces
import os
import sys
from typing import Callable, Any
import typing
import types
from enum import IntEnum
import dataclasses
import functools
import inspect
import base64
import datetime as dt
import json
import uuid
try:
import re2 as re # type: ignore
except ModuleNotFoundError:
import re # type: ignore
import flask
import werkzeug.exceptions as exceptions
from werkzeug.datastructures import FileStorage, CombinedMultiDict
import ProxyPatternPool as ppp # type: ignore
# for local use & forwarding
# NOTE the only missing should be "Flask"
from flask import (
Response, Request, request, session, Blueprint, make_response,
abort, redirect, url_for, after_this_request, send_file, current_app, g,
send_from_directory, render_template, get_flashed_messages,
has_app_context, has_request_context, render_template_string,
stream_with_context,
)
from importlib.metadata import version as pkg_version
import logging
log = logging.getLogger("fsa")
# get module version (should it be deprecated?)
__version__ = pkg_version("FlaskSimpleAuth")
class Hooks:
"""This class holds all hook types used by FlaskSimpleAuth."""
ErrorResponseFun = Callable[[str, int, dict[str, str]|None, str|None], Response]
"""Generate an error response for message and status.
:param description: description string of the error.
:param status: HTTP status code.
:param headers: dict of additional headers.
:param content_type: HTTP content type.
Must return a Response.
The function mimics flask.Response("message", status, headers, content_type).
"""
GetUserPassFun = Callable[[str], str|None]
"""Get password from user login, None if unknown.
:param login: user name to retrieve password for.
Returns the string, or None if no password for user.
"""
GroupCheckFun = Callable[[str], bool]
"""Tell whether a user belongs to some group.
:param login: user name.
Returns whether the user belongs to some group by calling
the appropriate callback.
"""
UserInGroupFun = Callable[[str, str|int], bool|None]
"""Is user login in group (str or int): yes, no, unknown.
:param login: user name to check for group membership.
:param group: group name or number to check for membership.
Returns whether the user belongs to the group.
This is a fallback for the previous per-group method.
"""
ObjectPermsFun = Callable[[str, Any, str|None], bool|None]
"""Check object access in domain, for parameter, in mode.
:param login: user name.
:param oid: object identifier, must a key.
:param mode: optional operation the user wants to perform on the object.
Returns whether permission is granted.
"""
PasswordCheckFun = Callable[[str, str], bool|None]
"""Low level check login/password validity.
:param login: user name.
:param password: the password as provided.
Returns whether password is valid for user.
"""
PasswordQualityFun = Callable[[str], bool|Any]
"""Is this password quality suitable?
:param password: the submitted password.
Returns whether the password is acceptable.
"""
CastFun = Callable[[str|Any], object]
"""Cast parameter value to some object.
:param data: initial data, usually a string.
Returns the converted object.
"""
SpecialParameterFun = Callable[[str], Any]
"""Generate a "special" parameter, with the parameter name.
:param name: parameter name (usually not needed).
:param ...: optional special parameters.
Returns an object which will be the parameter value.
"""
HeaderFun = Callable[[Response, str], str|None]
"""Add a header to the current response.
:param response: response to consider.
:param header: name of header.
Returns the header value, or *None*.
"""
BeforeRequestFun = Callable[[Request], Response|None]
"""Before request hook, with request provided.
:param request: current request.
Returns a response (to shortcut), or *None* to continue.
"""
BeforeExecFun = Callable[[Request, str|None, str|None], Response|None]
"""After authentication and right before execution.
:param request: current request.
:param login: user name of authenticated user.
:param auth: authentication scheme used.
"""
AfterRequestFun = Callable[[Response], Response]
"""After request hook."""
# FIXME Any is really FlaskSimpleAuth, but python lacks forward declarations
AuthenticationFun = Callable[[Any, Request], str|None]
"""Authentication hook.
:param app: current application.
:param request: current request.
Returns the authenticated user name, or *None*.
"""
JSONConversionFun = Callable[[Any], Any]
"""JSON conversion hook.
:param o: object of some type.
Returns a JSON serializable something from an object instance.
"""
PathCheckFun = Callable[[str, str], str|None]
"""Path checking hook.
:param method: method used on path.
:param path: path to be checked.
Allow to check path rules.
Returns an error message or *None* if all is well.
"""
@dataclasses.dataclass
class ErrorResponse(BaseException):
"""Exception class to carry fields for an error Response.
Use this exception from hooks to trigger an error response.
"""
# NOTE this should maybe inherit from exceptions.HTTPException?
message: str
status: int
headers: dict[str, str]|None = None
content_type: str|None = None
def err(*args, **kwargs):
"""Shorcut function to trigger an error response.
It can be used inside an expression, eg: ``_ = res or err("no data", 404)``
"""
raise ErrorResponse(*args, **kwargs)
class ConfigError(BaseException):
"""FSA User Configuration Error.
This error is raised on errors detected while initializing the application.
"""
pass
class _Mode(IntEnum):
"""FSA running modes."""
UNDEF = 0
PROD = 1
DEV = 2
DEBUG = 3
DEBUG1 = 3
DEBUG2 = 4
DEBUG3 = 5
DEBUG4 = 6
_MODES = {
"debug1": _Mode.DEBUG1,
"debug2": _Mode.DEBUG2,
"debug3": _Mode.DEBUG3,
"debug4": _Mode.DEBUG4,
"debug": _Mode.DEBUG,
"dev": _Mode.DEV,
"prod": _Mode.PROD,
}
#
# TYPE CASTS
#
class path(str):
"""Type to distinguish str path parameters.
Use this type as hint for a route parameter to trigger a Flask route path
parameter. A path may contain ``/`` characters.
"""
pass
class string(str):
"""Type to distinguish str string parameters.
Use this type as hint for a route parameter to trigger a Flask route string
parameter. A string may not contain ``/`` characters.
"""
pass
# "JsonData = json.loads" would do:-)
class JsonData:
"""Magic JSON parameter type.
This triggers interpretting a parameter as JSON when used as a parameter
type on a route.
"""
pass
class Session:
"""Session parameter type.
This provides the session object when used as a parameter type on a route.
"""
pass
class Globals:
"""Globals parameter type.
This provides the g (globals) object when used as a parameter type on a route.
"""
pass
class Environ:
"""Environ parameter type.
This provides the WSGI environ object when used as a parameter type on a route.
"""
pass
class CurrentUser(str):
"""CurrentUser parameter type.
This provides the authenticated user (str) when used as a parameter type on a route.
"""
pass
class CurrentApp:
"""CurrentApp parameter type.
This provides the current application object when used as a parameter type on a route.
"""
pass
class Cookie(str):
"""Application Cookie parameter type.
This provides the cookie value (str) when used as a parameter type on a route.
The `name` of the parameter is the cookie name.
"""
pass
class Header(str):
"""Request Header parameter type.
This provides the header value (str) when used as a parameter type on a route.
The `name` of the parameter is the header name (case insensitive, underscore for dash).
"""
pass
#
# SPECIAL PREDEFINED GROUP NAMES
#
ANY, ALL, NONE = "ANY", "ALL", "NONE" # deprecated constants and values…
_OPEN = {"OPEN", "ANY", "NOAUTH"}
"""Open route, no authentication."""
_AUTH = {"AUTH", "AUTHENTICATED", "ALL"}
"""Authenticated route."""
_CLOSE = {"CLOSE", "NONE", "NOBODY"}
"""Closed route."""
_PREDEFS = _OPEN | _AUTH | _CLOSE
"""All predefined pseudo-group names."""
_DEPRECATED_GROUPS = {"ANY", "ALL", "NONE"}
"""Deprecated pseudo-group names."""
def _is_predef(la: list[Any], s: set[str]):
return any(map(lambda i: i in s, la))
def _is_open(la: list[Any]):
return _is_predef(la, _OPEN)
def _is_auth(la: list[Any]):
return _is_predef(la, _AUTH)
def _is_close(la: list[Any]):
return _is_predef(la, _CLOSE)
def _is_optional(t) -> bool:
"""Tell whether type is marked as optional."""
return (
# T|None or None|Type
(isinstance(t, types.UnionType) and len(t.__args__) == 2 and
(t.__args__[0] == type(None) or t.__args__[0] is None or
t.__args__[1] == type(None) or t.__args__[1] is None)) or
(isinstance(t, types.UnionType) and len(t.__args__) == 2 and t.__args__[0] == type(None)) or
# Optional[T]
(hasattr(t, "__name__") and t.__name__ == "Optional") or # type: ignore
# Union[None, T] or Union[T, None]
(hasattr(t, "__origin__") and t.__origin__ is typing.Union and # type: ignore
len(t.__args__) == 2 and (t.__args__[0] == type(None) or t.__args__[1] == type(None)))
)
def _type(t) -> str:
"""Return type name for error message display."""
return type(t).__name__
def _valid_type(t) -> bool:
"""Return if type t is consistent with _check_type expectations."""
if t in (None, bool, int, float, str, types.NoneType):
return True
elif isinstance(t, types.GenericAlias):
if t.__name__ == "list":
assert len(t.__args__) == 1
return _valid_type(t.__args__[0])
elif t.__name__ == "dict":
assert len(t.__args__) == 2
ktype, vtype = t.__args__
return issubclass(ktype, str) and _valid_type(ktype) and _valid_type(vtype)
else: # TODO tuple set named-dict (?)
return False
elif isinstance(t, types.UnionType):
return all(_valid_type(a) for a in t.__args__)
elif hasattr(t, "__origin__") and t.__origin__ is typing.Union: # pragma: no cover
return any(_valid_type(a) for a in t.__args__)
elif hasattr(t, "__name__") and t.__name__ == "Optional": # type: ignore # pragma: no cover
assert len(t.__args__) == 1
return _valid_type(t.__args__[0])
else: # FIXME should accept reasonable types? Allow convertion?
return False
# TODO caster?
def _check_type(t, v) -> bool:
"""Dynamically and recursively check whether v is compatible with t."""
if t is None or t == types.NoneType:
return v is None
elif t == int: # beware that bool is also an int
return isinstance(v, int) and not isinstance(v, bool)
elif t in (bool, float, str): # simple types
return isinstance(v, t)
elif isinstance(t, types.GenericAlias): # generic types
if t.__name__ == "list":
assert len(t.__args__) == 1
item_type, = t.__args__
return isinstance(v, list) and all(_check_type(item_type, i) for i in v)
elif t.__name__ == "dict":
assert len(t.__args__) == 2
key_type, val_type = t.__args__
return isinstance(v, dict) and all(
_check_type(key_type, key) and _check_type(val_type, val) for key, val in v.items())
# TODO set? tuple?
else: # pragma: no cover
raise ValueError(f"unsupported generic type: {t.__name__}")
elif isinstance(t, types.UnionType): # |
return any(_check_type(a, v) for a in t.__args__)
elif hasattr(t, "__origin__") and t.__origin__ is typing.Union: # Union # pragma: no cover
return any(_check_type(a, v) for a in t.__args__)
elif hasattr(t, "__name__") and t.__name__ == "Optional": # type: ignore # pragma: no cover
assert len(t.__args__) == 1
return v is None or _check_type(t.__args__[0], v)
else: # whatever type
return isinstance(v, t)
def _is_list_of(t) -> Any:
if isinstance(t, types.GenericAlias) and t.__name__ == "list":
if len(t.__args__) == 1:
return t.__args__[0]
else: # pragma: no cover # cannot happen, "list" is not "list[*]"
return str
return None
def _is_generic_type(p: inspect.Parameter) -> bool:
"""Tell whether parameter is a generic type."""
a = p.annotation
if a is inspect._empty:
return False
elif _is_optional(a):
a = a.__args__[0]
return isinstance(a, (types.GenericAlias, types.UnionType))
def _typeof(p: inspect.Parameter):
"""Guess parameter type, possibly with some type inference."""
if p.kind is inspect.Parameter.VAR_KEYWORD: # **kwargs
return dict
elif p.kind is inspect.Parameter.VAR_POSITIONAL: # *args
return list
elif p.annotation is not inspect._empty:
anno = p.annotation
if _is_optional(anno): # skip optional (3 forms)
a = anno.__args__[0]
if a in (None, types.NoneType):
a = anno.__args__[1]
else:
a = anno
return a
elif p.default and p.default is not inspect._empty:
return type(p.default)
else:
return str
def _json_prepare(a: Any):
"""Extended JSON conversion for Flask."""
# special cases for data structures
if hasattr(a, "model_dump"): # Pydantic BaseModel
return a.model_dump()
elif hasattr(a, "__pydantic_fields__"): # Pydantic dataclass
return dataclasses.asdict(a)
elif hasattr(a, "__dataclass_fields__"): # standard dataclass
return dataclasses.asdict(a)
else: # do nothing, rely on flask's jsonify
return a
def _json_stream(gen):
"""Stream a generator output as a JSON array.
This may or may not be a good idea depending on the database driver and
WSGI server behavior. To ensure a direct string output, consider setting
``FSA_JSON_STREAMING`` to false.
"""
yield "["
comma = False
for i in gen:
if comma:
yield ","
else:
comma = True
yield flask.json.dumps(_json_prepare(i))
yield "]\n"
_fsa_json_streaming = True
class _JSONProvider(flask.json.provider.DefaultJSONProvider): # type: ignore
"""FlaskSimpleAuth Internal JSON Provider.
Convertion to str based on types for date, datetime, time, timedelta,
timezone and UUID.
:param allstr: whether convert unexpected types to ``str``.
"""
def __init__(self, app, allstr: bool = False):
super().__init__(app)
self._typemap: dict[Any, Hooks.JSONConversionFun] = {
# override defaults to avoid English-specific RFC822
dt.date: str,
dt.datetime: str,
# add missing types
dt.time: str,
dt.timedelta: str,
dt.timezone: str,
uuid.UUID: str,
}
self._skip: tuple[type, ...] = tuple()
_ = allstr and self._set_allstr()
def set_allstr(self):
self._skip = (str, float, bool, int, list, tuple, dict, type(None))
def add_converter(self, t: Any, h: Hooks.JSONConversionFun):
self._typemap[t] = h
def default(self, o: Any):
"""Extended JSON conversion for Flask."""
# special cases for data structures
if hasattr(o, "model_dump"): # Pydantic BaseModel # pragma: no cover
return o.model_dump()
elif hasattr(o, "__pydantic_fields__"): # Pydantic dataclass
return dataclasses.asdict(o)
elif hasattr(o, "__dataclass_fields__"): # standard dataclass # pragma: no cover
return dataclasses.asdict(o)
else:
if encoder := self._typemap.get(type(o), None):
return encoder(o)
elif self._skip and not isinstance(o, self._skip):
return str(o)
else: # FIXME # pragma: no cover
super().default(o)
def jsonify(a: Any) -> Response:
"""Jsonify something, including generators, dataclasses and pydantic stuff.
This is somehow an extension of Flask own jsonify, although it takes only
one argument.
NOTE on generators, the generator output is json-streamed instead of being
treated as a string or bytes generator.
"""
if isinstance(a, Response):
return a
elif inspect.isgenerator(a) or type(a) in (map, filter, range):
out = _json_stream(a)
if not _fsa_json_streaming: # switch to string
out = "".join(out)
return Response(out, mimetype="application/json")
else:
return flask.jsonify(_json_prepare(a))
def checkPath(method: str, path: str) -> str|None:
"""Convenient function to use as a path checking hook.
The path must only contain lower-case ascii characters possibly interspersed
with dashes `-`, and not contain method names.
"""
if not re.match(r"(/[a-z]+(-[a-z]+)*|/<[^>]+>)+", path):
return f"invalid path section: {path}"
if re.search(r"\b(get|post|put|patch|delete)\b", path, re.I):
return f"path contains a method name: {path}"
class Reference(ppp.Proxy):
"""Convenient object wrapper class.
This is a very thin wrapper around ProxyPatternPool Proxy class.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Flask(flask.Flask):
"""Flask class wrapper.
The class behaves mostly as a Flask class, but supports extensions:
- the ``route`` decorator manages authentication, authorization and
parameters transparently.
- per-methods shortcut decorators allow to handle root for a given
method: ``get``, ``post``, ``put``, ``patch``, ``delete``.
- ``make_response`` slightly extends its parent to allow changing
the default content type and handle *None* body.
- several additional methods are provided: ``get_user_pass``,
``user_in_group``, ``group_check``, ``check_password``, ``hash_password``,
``create_token``, ``get_user``, ``current_user``, ``clear_caches``,
``cast``, ``object_perms``, ``user_scope``, ``password_quality``,
``password_check``, ``add_group``, ``add_scope``, ``add_headers``,
``error_response``, ``authentication``…
See ``FlaskSimpleAuth`` class documentation about these methods.
"""
def __init__(self, *args, debug: bool = False, **kwargs):
# extract FSA-specific directives
fsaconf: dict[str, Any] = {}
for key, val in kwargs.items():
if key.startswith("FSA_"):
fsaconf[key] = val
for key in fsaconf:
del kwargs[key]
# Flask actual initialization
super().__init__(*args, **kwargs)
# FSA extension initialization
self._fsa = FlaskSimpleAuth(self, debug=debug, **fsaconf)
# overwritten late because called by upper Flask initialization for "static"
setattr(self, "add_url_rule", self._fsa.add_url_rule)
# forward hooks
self.error_response = self._fsa.error_response
self.get_user_pass = self._fsa.get_user_pass
self.user_in_group = self._fsa.user_in_group
self.group_check = self._fsa.group_check
self.object_perms = self._fsa.object_perms
self.user_scope = self._fsa.user_scope
# decorators
self.cast = self._fsa.cast
self.special_parameter = self._fsa.special_parameter
self.password_quality = self._fsa.password_quality
self.password_check = self._fsa.password_check
self.add_group = self._fsa.add_group
self.add_scope = self._fsa.add_scope
self.add_headers = self._fsa.add_headers
self.before_exec = self._fsa.before_exec
self.authentication = self._fsa.authentication
self.add_json_converter = self._fsa.add_json_converter
# forward methods
self.check_password = self._fsa.check_password
self.check_user_password = self._fsa.check_user_password
self.hash_password = self._fsa.hash_password
self.create_token = self._fsa.create_token
self.get_user = self._fsa.get_user
self.current_user = self._fsa.current_user
self.clear_caches = self._fsa.clear_caches
self.password_uncache = self._fsa.password_uncache
self.token_uncache = self._fsa.token_uncache
self.group_uncache = self._fsa.group_uncache
self.object_perms_uncache = self._fsa.object_perms_uncache
self.user_token_uncache = self._fsa.user_token_uncache
self.auth_uncache = self._fsa.auth_uncache
self.path_check = self._fsa.path_check
# overwrite decorators ("route" done through add_url_rule above)
setattr(self, "get", self._fsa.get) # FIXME avoid mypy warnings
setattr(self, "put", self._fsa.put)
setattr(self, "post", self._fsa.post)
setattr(self, "patch", self._fsa.patch)
setattr(self, "delete", self._fsa.delete)
# json provider
self.json = self._fsa._app.json
def make_response(self, rv) -> Response:
"""Create a Response.
This method handles overriding the default ``Content-Type`` and accepts
a *None* body.
"""
# handle None body as empty
if rv is None:
rv = ("", 200)
elif isinstance(rv, tuple) and rv[0] is None:
rv = ("",) + rv[1:]
# use flask to create a response
res = super().make_response(rv)
# possibly override Content-Type header
if self._fsa._rm._default_type:
val = rv[0] if isinstance(rv, tuple) else rv
if type(val) in (bytes, str):
res.content_type = self._fsa._rm._default_type if len(val) else "text/plain"
elif type(rv) in (typing.Generator, typing.Iterator): # pragma: no cover
res.content_type = self._fsa._rm._default_type
return res
# significant default settings are centralized here
class Directives:
"""Documentation for configuration directives.
This class presents *all* configuration directives, their expected type and
default value.
"""
# debugging and deprecation
FSA_MODE: str = "prod"
"""Execution mode.
- ``prod``: default terse mode.
- ``dev``: adds headers with the route, authentication and run time.
- ``debug1`` to ``debug4``: increasing debug.
"""
FSA_LOGGING_LEVEL: int = logging.INFO
"""Module internal logging level.
Upgrade to ``logging.DEBUG`` for maximal verbosity.
"""
FSA_ALLOW_DEPRECATION: bool = False
"""Whether to allow deprecated features.
Default is *False*, meaning deprecated features are coldly rejected.
On *True*, a warning is generated when the feature is encountered.
This setting may or may not apply to anything depending on the version.
"""
# general settings
FSA_SECURE: bool = True
"""Require TLS on non local connexions.
This should be *True*, unless an external appliance handles TLS decryption.
"""
FSA_SERVER_ERROR: int = 500
"""Status code on FSA internal server errors.
This is for debugging help.
Changing this allows to separate FSA errors from Flask errors or others.
"""
FSA_NOT_FOUND_ERROR: int = 404
"""Status code on not found errors.
This is for debugging help.
Changing this allows to separate FSA generated 404 from others.
"""
FSA_LOCAL: str = "thread"
"""Isolation requirement for internal per-request objects.
- ``process``: one process only
- ``thread``: threaded request handler
- ``werkzeug``: use werkzeug local
- ``gevent``: gevent request handler
- ``eventlet``: eventlet request handler
Depending on the WSGI server, requests may be managed by process,
thread, greenlet… this setting must match the WGI context so that FSA
can isolate requests properly.
"""
FSA_HANDLE_ALL_ERRORS: bool = True
"""Whether to handle all 4xx and 5xx Flask-generated errors.
- on *True*: override Flask error processing to use FlaskSimpleAuth
response generation with FSA internal error handler (FSA_ERROR_RESPONSE).
- on *False*: some errors may generate their own response in any format
based on Flask default error response generator.
"""
FSA_KEEP_USER_ERRORS: bool = False
"""Whether to hide user errors.
They may occur from any user-provided functions such as various hooks and
route functions.
- on *False*: intercept user errors and turned them into 5xx.
- on *True*: refrain from handling user errors and let them pass to the
outer WSGI infrastructure instead. User errors are intercepted anyway,
traced and raised again.
"""
# register hooks
FSA_ERROR_RESPONSE: str|Hooks.ErrorResponseFun = "plain"
"""Common hook for generating a response on errors.
Same as ``error_response`` decorator.
- ``plain``: generate a simple *text/plain* response.
- ``json``: generate a simple *application/json* string.
- ``json:msg``: generate a JSON object with property ``msg``.
- *callback*: give full control to a callback which is passed
the message, the status, headers and content type.
"""
FSA_GET_USER_PASS: Hooks.GetUserPassFun|None = None
"""Password hook for getting a user's salted hashed password.
Same as ``get_user_pass`` decorator.
Provide a callback to retrieved the hashed password from the user login.
Returning *None* will skip internal password checking.
"""
FSA_AUTHENTICATION: dict[str, Hooks.AuthenticationFun] = {}
"""Authentication hook for adding authentication schemes.
Same as ``authentication`` decorator.
For each scheme name, associate a callback which will be given the app and
request, and should return the authenticated user login (str).
Returning *None* suggests a 401 for this scheme.
The implementation may also raise an ``ErrorResponse``.
"""
FSA_GROUP_CHECK: dict[str, Hooks.GroupCheckFun] = {}
"""Authorization hook for checking whether a user is some groups.
Same as ``group_check`` decorator.
For each group name, associate a callback which given a login returns
whether the user belongs to this group.
The group name is also registered in passing.
"""
FSA_USER_IN_GROUP: Hooks.UserInGroupFun|None = None
"""Authorization hook for checking a user group.
Same as ``user_in_group`` decorator.
Provide a hook to check whether a user, identified by their login, belogs
to a group.
"""
FSA_OBJECT_PERMS: dict[str, Hooks.ObjectPermsFun] = {}
"""Authorization hook for object permissions.
Same as ``authorization`` decorator.
For each kind of object (domain), associate a callback which is given
the object id, the user login and the expected role, and returns whether
the user has this role for this object id. Return *None* for 404.
"""
FSA_CAST: dict[Any, Hooks.CastFun] = {}
"""Parameter hook for type conversion.
Cast function to call on the raw parameter (usually) string value,
if it does not have the expected type.
This does not apply to special and pydantic parameters.
See also ``cast`` function/decorator.
"""
FSA_SPECIAL_PARAMETER: dict[Any, Hooks.SpecialParameterFun] = {}
"""Parameter hook for special parameters.
The hook is called with the parameter *name* as an argument.
It may access ``request`` or whatever to return some value.
See also ``special_parameter`` function/decorator.
"""
# FIXME there should be a decorator as well?
FSA_BEFORE_REQUEST: list[Hooks.BeforeRequestFun] = []
"""Request hook executed before request.
These hooks are managed internally by FlaskSimpleAuth so that they are
executed *after* its own (FSA) before request hooks, so as to minimize
interactions between user hooks registered to Flask directly and its own
hooks.
"""
FSA_BEFORE_EXEC: list[Hooks.BeforeExecFun] = []
"""Request hook executed after authentication.
FlaskSimpleAuth-specific hooks executed after authentication, so that for
instance the current user is available.
The hook is executed *after* authentication and *before* the user function.
It may be used to commit and return a database connection used by the
authentication phase.
See also ``before_exec`` function/decorator.
"""
FSA_AFTER_REQUEST: list[Hooks.AfterRequestFun] = []
"""Request hook executed after request.
These hooks are managed internally by FlaskSimpleAuth so that they are
executed *after* its own before request hooks, so as to minimize
interactions between user hooks registered to Flask directly and its own
hooks.
"""
FSA_ADD_HEADERS: dict[str, str|Callable[[], str]] = {}
"""Response hook to add headers.
Key is the header name, value is the header value or a function generating
the header value.
See also ``add_headers`` function.
"""
# authentication
FSA_AUTH: str|list[str] = []
"""List of enabled authentication schemes.
This directive is **mandatory**.
Note: the result of authentication is the user identification (eg login,
name or email…) as a string, which is accessible from the application and
using the ``CurrentUser`` special parameter type in route functions.
- ``none``: no authentication, implicit if ``FSA_AUTH`` is a scalar, required for OPEN routes.
- ``httpd``: inherit web-server authentication.
- ``basic``: HTTP Basic password authentication.
- ``http-basic``: same with *Flask-HTTPAuth* implementation.
- ``digest``: HTTP Digest password authentication with *Flask-HTTPAuth*.
- ``http-digest``: same as previous.
- ``param``: parameter password authentication.
- ``password``: try ``basic`` then ``param``.
- ``fake``: fake authentication using a parameter, for local tests only.
- ``token``: token authentication (implicit if ``FSA_AUTH`` is a scalar).
- ``http-token``: same with *Flask-HTTPAuth*.
- ``oauth``: token authentication variant, where the token holds the list of permissions.
"""
FSA_AUTH_DEFAULT: str|list[str]|None = None
"""Default authentications to use on a route.
These authentications **must** be enabled.
Default is *None*, which means relying on schemes allowed by ``FSA_AUTH``.
"""
FSA_REALM: str = "<to be set as application name>"
"""Authentication realm, default is application name.
This realm is used for *basic*, *digest* and *token* authentications.
"""
FSA_FAKE_LOGIN: str = "LOGIN"
"""Parameter name for fake authentication.
This parameter string value is taken as the authenticated user name when
*fake* auth is enabled. Only for local tests, please!
"""
FSA_PARAM_USER: str = "USER"
"""Parameter name for user for param authentication.
This parameter string value is the login name for *param* authentication.
"""
FSA_PARAM_PASS: str = "PASS"
"""Parameter name for password for param authentication.
This parameter string value is the password for *param* authentication.
"""
FSA_TOKEN_TYPE: str|None = "fsa"
"""Type of authentication token.
- ``fsa``: simple custom token
- ``jwt``: JSON web token standard
- *None*: disable token authentication
"""