-
Notifications
You must be signed in to change notification settings - Fork 131
/
Copy pathbase.py
479 lines (408 loc) · 17.9 KB
/
base.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
"""
The SATOSA main module
"""
import json
import logging
import uuid
from saml2.s_utils import UnknownSystemEntity
from satosa import util
from satosa.response import BadRequest
from satosa.response import NotFound
from satosa.response import Redirect
from .context import Context
from .exception import SATOSAError, SATOSAAuthenticationError, SATOSAUnknownError
from .plugin_loader import load_backends, load_frontends
from .plugin_loader import load_request_microservices, load_response_microservices
from .plugin_loader import load_database
from .routing import ModuleRouter, SATOSANoBoundEndpointError
from .state import cookie_to_state, SATOSAStateError, State, state_to_cookie
from .exception import SATOSAAuthenticationError
from .exception import SATOSAAuthenticationFlowError
from .exception import SATOSABadRequestError
from .exception import SATOSAError
from .exception import SATOSAMissingStateError
from .exception import SATOSANoBoundEndpointError
from .exception import SATOSAUnknownError
from .exception import SATOSAStateError
from .plugin_loader import load_backends
from .plugin_loader import load_frontends
from .plugin_loader import load_request_microservices
from .plugin_loader import load_response_microservices
from .routing import ModuleRouter
from .state import State
from .state import cookie_to_state
from .state import state_to_cookie
import satosa.logging_util as lu
logger = logging.getLogger(__name__)
STATE_KEY = "SATOSA_BASE"
class SATOSABase(object):
"""
Base class for a satosa proxy server.
Does not contain any server parts.
"""
def __init__(self, config):
"""
Creates a satosa proxy base
:type config: satosa.satosa_config.SATOSAConfig
:param config: satosa proxy config
"""
self.config = config
logger.info("Loading backend modules...")
backends = load_backends(self.config, self._auth_resp_callback_func,
self.config["INTERNAL_ATTRIBUTES"],
self._logout_resp_callback_func
)
logger.info("Loading frontend modules...")
frontends = load_frontends(self.config, self._auth_req_callback_func,
self.config["INTERNAL_ATTRIBUTES"],
self._logout_req_callback_func
)
self.response_micro_services = []
self.request_micro_services = []
logger.info("Loading micro services...")
if "MICRO_SERVICES" in self.config:
self.request_micro_services.extend(load_request_microservices(
self.config.get("CUSTOM_PLUGIN_MODULE_PATHS"),
self.config["MICRO_SERVICES"],
self.config["INTERNAL_ATTRIBUTES"],
self.config["BASE"]))
self._link_micro_services(self.request_micro_services, self._auth_req_finish)
self.response_micro_services.extend(
load_response_microservices(self.config.get("CUSTOM_PLUGIN_MODULE_PATHS"),
self.config["MICRO_SERVICES"],
self.config["INTERNAL_ATTRIBUTES"],
self.config["BASE"]))
self._link_micro_services(self.response_micro_services, self._auth_resp_finish)
load_db = self.config.get("LOGOUT_ENABLED", False)
if load_db:
logger.info("Loading database...")
self.db = load_database(self.config)
self.module_router = ModuleRouter(frontends, backends,
self.request_micro_services + self.response_micro_services)
def _link_micro_services(self, micro_services, finisher):
if not micro_services:
return
for i in range(len(micro_services) - 1):
micro_services[i].next = micro_services[i + 1].process
micro_services[-1].next = finisher
def _auth_req_callback_func(self, context, internal_request):
"""
This function is called by a frontend module when an authorization request has been
processed.
:type context: satosa.context.Context
:type internal_request: satosa.internal.InternalData
:rtype: satosa.response.Response
:param context: The request context
:param internal_request: request processed by the frontend
:return: response
"""
state = context.state
state[STATE_KEY] = {"requester": internal_request.requester}
msg = "Requesting provider: {}".format(internal_request.requester)
logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg)
logger.info(logline)
if self.request_micro_services:
return self.request_micro_services[0].process(context, internal_request)
return self._auth_req_finish(context, internal_request)
def _logout_req_callback_func(self, context, internal_request):
"""
This function is called by a frontend module when a logout request has been processed.
:type context: satosa.context.Context
:typr internal_request:
:rtype:
:param context: The request context
:param internal_request: request processed by the frontend
:return Response
"""
state = context.state
state[STATE_KEY] = {"requester": internal_request.requester}
msg = "Requesting provider: {}".format(internal_request.requester)
logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg)
logger.info(logline)
return self._logout_req_finish(context, internal_request)
def _auth_req_finish(self, context, internal_request):
backend = self.module_router.backend_routing(context)
context.request = None
return backend.start_auth(context, internal_request)
def _logout_req_finish(self, context, internal_request):
backend = self.module_router.backend_routing(context)
context.request = None
if hasattr(self, "db"):
internal_authn_resp = self.db.get_authn_resp(context.state)
self.db.delete_session(context.state)
else:
internal_authn_resp = None
context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True)
return backend.start_logout(context, internal_request, internal_authn_resp)
def _auth_resp_finish(self, context, internal_response):
user_id_to_attr = self.config["INTERNAL_ATTRIBUTES"].get("user_id_to_attr", None)
if user_id_to_attr:
internal_response.attributes[user_id_to_attr] = [internal_response.subject_id]
# remove all session state unless CONTEXT_STATE_DELETE is False
context.request = None
if hasattr(self, "db"):
self.db.store_authn_resp(context.state, internal_response)
self.db.get_authn_resp(context.state)
else:
context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True)
frontend = self.module_router.frontend_routing(context)
return frontend.handle_authn_response(context, internal_response)
def _logout_resp_finish(self, context):
context.request = None
frontend = self.module_router.frontend_routing(context)
return frontend.handle_logout_response(context)
def _auth_resp_callback_func(self, context, internal_response):
"""
This function is called by a backend module when the authorization is
complete.
:type context: satosa.context.Context
:type internal_response: satosa.internal.InternalData
:rtype: satosa.response.Response
:param context: The request context
:param internal_response: The authentication response
:return: response
"""
context.request = None
internal_response.requester = context.state[STATE_KEY]["requester"]
# If configured construct the user id from attribute values.
if "user_id_from_attrs" in self.config["INTERNAL_ATTRIBUTES"]:
subject_id = [
"".join(internal_response.attributes[attr]) for attr in
self.config["INTERNAL_ATTRIBUTES"]["user_id_from_attrs"]
]
internal_response.subject_id = "".join(subject_id)
if self.response_micro_services:
return self.response_micro_services[0].process(
context, internal_response)
return self._auth_resp_finish(context, internal_response)
def _logout_resp_callback_func(self, context):
"""
This function is called by a backend module when logout is complete
:type context: satosa.context.Context
:type internal_response: satosa.internal.LogoutInformation
:rtype: satosa.response.Response
:param context: The request context
:param internal_response: The logout response
"""
context.request = None
context.state["ROUTER"] = "idp"
return self._logout_resp_finish(context)
def _handle_satosa_authentication_error(self, error):
"""
Sends a response to the requester about the error
:type error: satosa.exception.SATOSAAuthenticationError
:rtype: satosa.response.Response
:param error: The exception
:return: response
"""
context = Context()
context.state = error.state
frontend = self.module_router.frontend_routing(context)
return frontend.handle_backend_error(error)
def _run_bound_endpoint(self, context, spec):
"""
:type context: satosa.context.Context
:type spec: ((satosa.context.Context, Any) -> satosa.response.Response, Any) |
(satosa.context.Context) -> satosa.response.Response
:param context: The request context
:param spec: bound endpoint function
:return: response
"""
try:
return spec(context)
except SATOSAAuthenticationError as error:
error.error_id = uuid.uuid4().urn
state = json.dumps(error.state.state_dict, indent=4)
msg = "ERROR_ID [{err_id}]\nSTATE:\n{state}".format(
err_id=error.error_id, state=state
)
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline, exc_info=True)
return self._handle_satosa_authentication_error(error)
def _load_state(self, context):
"""
Load state from cookie to the context
:type context: satosa.context.Context
:param context: Session context
"""
try:
state = cookie_to_state(
context.cookie,
self.config["COOKIE_STATE_NAME"],
self.config["STATE_ENCRYPTION_KEY"],
)
except SATOSAStateError:
state = State()
finally:
context.state = state
msg = f"Loaded state {state} from cookie {context.cookie}"
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.debug(logline)
def _save_state(self, resp, context):
"""
Saves a state from context to cookie
:type resp: satosa.response.Response
:type context: satosa.context.Context
:param resp: The response
:param context: Session context
"""
cookie_name = self.config["COOKIE_STATE_NAME"]
cookie = state_to_cookie(
context.state,
name=cookie_name,
path="/",
encryption_key=self.config["STATE_ENCRYPTION_KEY"],
secure=self.config.get("COOKIE_SECURE"),
httponly=self.config.get("COOKIE_HTTPONLY"),
samesite=self.config.get("COOKIE_SAMESITE"),
max_age=self.config.get("COOKIE_MAX_AGE"),
)
resp.headers = [
(name, value)
for (name, value) in resp.headers
if name != "Set-Cookie"
or not value.startswith(f"{cookie_name}=")
]
resp.headers.append(tuple(cookie.output().split(": ", 1)))
def run(self, context):
"""
Runs the satosa proxy with the given context.
:type context: satosa.context.Context
:rtype: satosa.response.Response
:param context: The request context
:return: response
"""
try:
self._load_state(context)
spec = self.module_router.endpoint_routing(context)
resp = self._run_bound_endpoint(context, spec)
self._save_state(resp, context)
except SATOSABadRequestError as e:
error_id = uuid.uuid4().urn
msg = {
"message": "Bad Request",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
redirect_url = f"{generic_error_url}?errorid={error_id}"
return Redirect(generic_error_url)
return BadRequest(error)
except SATOSAMissingStateError as e:
error_id = uuid.uuid4().urn
msg = {
"message": "Missing SATOSA State",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
redirect_url = f"{generic_error_url}?errorid={error_id}"
return Redirect(generic_error_url)
raise
except SATOSAAuthenticationFlowError as e:
error_id = uuid.uuid4().urn
msg = {
"message": "SATOSA Authentication Flow Error",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
redirect_url = f"{generic_error_url}?errorid={error_id}"
return Redirect(generic_error_url)
raise
except SATOSANoBoundEndpointError as e:
error_id = uuid.uuid4().urn
msg = {
"message": "URL-path is not bound to any endpoint function",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
redirect_url = f"{generic_error_url}?errorid={error_id}"
return Redirect(generic_error_url)
return NotFound("The Service or Identity Provider you requested could not be found.")
except SATOSAError as e:
error_id = uuid.uuid4().urn
msg = {
"message": "Uncaught SATOSA error",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
redirect_url = f"{generic_error_url}?errorid={error_id}"
return Redirect(generic_error_url)
raise
except UnknownSystemEntity as e:
error_id = uuid.uuid4().urn
msg = {
"message": "Configuration error: unknown system entity",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
redirect_url = f"{generic_error_url}?errorid={error_id}"
return Redirect(generic_error_url)
raise
except Exception as e:
error_id = uuid.uuid4().urn
msg = {
"message": "Uncaught exception",
"error": str(e),
"error_id": error_id,
}
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.error(logline)
generic_error_url = self.config.get("ERROR_URL")
if generic_error_url:
return Redirect(generic_error_url)
raise SATOSAUnknownError("Unknown error") from e
else:
return resp
class SAMLBaseModule(object):
KEY_ENTITYID_ENDPOINT = 'entityid_endpoint'
KEY_ENABLE_METADATA_RELOAD = 'enable_metadata_reload'
KEY_ATTRIBUTE_PROFILE = 'attribute_profile'
KEY_ACR_MAPPING = 'acr_mapping'
VALUE_ATTRIBUTE_PROFILE_DEFAULT = 'saml'
def init_config(self, config):
self.attribute_profile = config.get(
self.KEY_ATTRIBUTE_PROFILE,
self.VALUE_ATTRIBUTE_PROFILE_DEFAULT)
self.acr_mapping = config.get(self.KEY_ACR_MAPPING)
return config
def expose_entityid_endpoint(self):
value = self.config.get(self.KEY_ENTITYID_ENDPOINT, False)
return bool(value)
def enable_metadata_reload(self):
"""
Check whether metadata reload has been enabled in config
return: bool
"""
value = self.config.get(self.KEY_ENABLE_METADATA_RELOAD, False)
return bool(value)
class SAMLEIDASBaseModule(SAMLBaseModule):
VALUE_ATTRIBUTE_PROFILE_DEFAULT = 'eidas'
def init_config(self, config):
config = super().init_config(config)
spec_eidas = {
'entityid_endpoint': True,
}
return util.check_set_dict_defaults(config, spec_eidas)