Coverage for gws-app/gws/base/auth/manager.py: 89%
131 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""Authorization and session manager."""
3from typing import Optional, cast
5import gws
6import gws.config
7import gws.lib.jsonx
9from . import session, system_provider, throttle
12class Config(gws.Config):
13 """Authentication and authorization options"""
15 methods: Optional[list[gws.ext.config.authMethod]]
16 """Authorization methods."""
17 providers: Optional[list[gws.ext.config.authProvider]]
18 """Authorization providers."""
19 mfa: Optional[list[gws.ext.config.authMultiFactorAdapter]]
20 """Authorization providers."""
21 session: Optional[gws.ext.config.authSessionManager]
22 """Session options."""
23 throttle: Optional[throttle.Config]
24 """Authentication throttle options. (added in 8.4)"""
27_DEFAULT_SESSION_TYPE = 'sqlite'
30class Object(gws.AuthManager):
31 """Authorization manager."""
33 def configure(self):
34 self.sessionMgr = self.create_child(gws.ext.object.authSessionManager, self.cfg('session'), type=_DEFAULT_SESSION_TYPE)
36 self.throttle = None
37 p = self.cfg('throttle')
38 if p:
39 self.throttle = cast(throttle.Object, self.create_child(throttle.Object, p))
41 self.providers = self.create_children(gws.ext.object.authProvider, self.cfg('providers'))
43 sys_provider = self.create_child(system_provider.Object)
44 self.add_provider(sys_provider)
46 self.guestUser = sys_provider.get_user('guest')
47 self.systemUser = sys_provider.get_user('system')
49 self.methods = self.create_children(gws.ext.object.authMethod, self.cfg('methods'))
50 if not self.methods:
51 # if no methods configured, enable the Web method
52 self.add_method(self.create_child(gws.ext.object.authMethod, type='web'))
54 self.mfAdapters = self.create_children(gws.ext.object.authMultiFactorAdapter, self.cfg('mfa'))
56 self.guestSession = session.Object(uid='guest_session', method=None, user=self.guestUser)
58 self.root.app.middlewareMgr.register(self, 'auth', depends_on=['db'])
60 ##
62 def add_provider(self, provider):
63 self.providers.append(provider)
65 def add_method(self, method):
66 self.methods.append(method)
68 def add_multi_factor_adapter(self, adapter):
69 self.mfAdapters.append(adapter)
71 ##
73 def enter_middleware(self, req: gws.WebRequester):
74 sess = self._try_open_session(req)
75 if sess:
76 gws.log.debug(f'session: user={req.session.user.uid!r} roles={req.session.user.roles}')
77 req.set_session(sess)
78 else:
79 gws.log.debug('session: guest')
80 req.set_session(self.guestSession)
82 def _try_open_session(self, req):
83 for meth in self.methods:
84 if not self.can_use_method(req, meth):
85 gws.log.warning(f'open_session: {meth=}: insecure_context, ignore')
86 continue
88 sess = meth.open_session(req)
89 if not sess:
90 continue
92 if not sess.user:
93 gws.log.warning(f'open_session: {meth=}: {sess.uid=} user not found')
94 self.sessionMgr.delete(sess)
95 return
97 if not sess.method or sess.method.uid != meth.uid:
98 gws.log.warning(f'open_session: {meth=}: {sess.uid=} wrong method {sess.method=}')
99 self.sessionMgr.delete(sess)
100 return
102 gws.log.debug(f'open_session: {meth=}: ok')
103 return sess
105 def exit_middleware(self, req: gws.WebRequester, res: gws.WebResponder):
106 sess = req.session
107 if sess.method:
108 sess.method.close_session(req, res)
109 req.set_session(self.guestSession)
111 def can_use_method(self, req, meth):
112 if not meth.secure or req.isSecure:
113 return True
114 if not meth.allowInsecureFrom:
115 return False
116 if req.ip not in meth.allowInsecureFrom:
117 return False
118 gws.log.warning(f'open_session: {meth=}: insecure_context allowed from {req.ip=}')
119 return True
121 ##
123 def create_transient_session(self, method, user, data=None):
124 return session.Object(
125 uid=gws.u.random_string(64),
126 method=method,
127 user=user,
128 data=data,
129 is_changed=False,
130 is_transient=True,
131 )
133 ##
135 def authenticate(self, method, credentials, req):
136 if not self.throttle:
137 return self._authenticate2(method, credentials)
139 sec = self.throttle.blocked_for(req, method, credentials)
140 if sec > 0:
141 gws.log.warning(f'authenticate: {method=}: throttled for {sec}s')
142 exc = gws.TooManyRequestsError('too many authentication attempts')
143 exc.retryAfter = sec
144 raise exc
146 try:
147 user = self._authenticate2(method, credentials)
148 except gws.ForbiddenError:
149 self.throttle.register(False, req, method, credentials)
150 raise
152 self.throttle.register(user is not None, req, method, credentials)
153 return user
155 def _authenticate2(self, method, credentials):
156 for prov in self.providers:
157 if prov.allowedMethods and method.extType not in prov.allowedMethods:
158 continue
159 gws.log.debug(f'trying provider {prov!r}')
160 user = prov.authenticate(method, credentials)
161 if user:
162 gws.log.debug(f'ok provider {prov!r}')
163 return user
165 ##
167 def get_user(self, user_uid):
168 provider_uid, local_uid = gws.u.split_uid(user_uid)
169 prov = self.get_provider(provider_uid)
170 return prov.get_user(local_uid) if prov else None
172 def get_provider(self, uid):
173 for obj in self.providers:
174 if obj.uid == uid:
175 return obj
177 def get_method(self, uid=None, ext_type=None):
178 for obj in self.methods:
179 if obj.uid == uid:
180 return obj
182 def get_multi_factor_adapter(self, uid=None, ext_type=None):
183 for obj in self.mfAdapters:
184 if obj.uid == uid:
185 return obj
187 def serialize_user(self, user):
188 return gws.lib.jsonx.to_string([user.authProvider.uid, user.authProvider.serialize_user(user)])
190 def unserialize_user(self, data):
191 provider_uid, ds = gws.lib.jsonx.from_string(data)
192 prov = self.get_provider(provider_uid)
193 return prov.unserialize_user(ds) if prov else None
195 def is_public_object(self, obj, *context):
196 return self.guestUser.can_read(obj, *context)