Skip to content

Commit a2a4698

Browse files
committed
Error reporting clarified: CRITICAL level for unexpected conditions in saml2test; ERROR and lower for conditions in the test target; various documentation improvements
1 parent 781fbe4 commit a2a4698

File tree

8 files changed

+248
-88
lines changed

8 files changed

+248
-88
lines changed

doc/sp_test/internal.rst

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ These files should be stored outside the saml2test package to have a clean separ
2424
Client (sp_test/__init__.py)
2525
.........................
2626
Its life cycle is responsible for following activites:
27-
- read config files and command line argumants
27+
- read config files and command line argumants (the test driver's config is "json_config")
2828
- initialize the test driver IDP
2929
- initialize a Conversation
3030
- start the Conversion with .do_sequence_and_tests()
@@ -33,54 +33,55 @@ Its life cycle is responsible for following activites:
3333
Conversation (sp_test/base.py)
3434
..............................
3535

36-
operation (oper)
36+
Operation (oper)
3737
................
38-
- comprises an id, name, sequence and tests
39-
- names oper in
38+
- Comprises an id, name, sequence and tests
4039
- Example: 'sp-00': {"name": 'Basic Login test', "sequence": [(Login, AuthnRequest, AuthnResponse, None)], "tests": {"pre": [], "post": []}
41-
- Names a use case in STHREP
4240

4341
OPERATIONS
4442
..........
4543
- set of operations provided in sp_test
4644
- can be listed with the -l command line option
4745

48-
sequence
46+
Sequence
4947
........
5048
- A list of flows
51-
- Example: see "sequence" item in operation dictionary
49+
- Example: see "sequence" item in operation dict
5250

53-
test (in the context of an operation)
51+
Test (in the context of an operation)
5452
....
55-
- class to be executed as part of an operation, either before ("pre") or after ("post") the sequence
53+
- class to be executed as part of an operation, either before ("pre") or after ("post") the sequence or inbetween a SAML request and response ("mid").
54+
There are standard tests with the Request class (VerifyAuthnRequest) and operation-specific tests.
55+
- Example for an operation-specific "mid" test: VerifyIfRequestIsSigned
56+
- A test may be specified together with an argument as a tupel
5657

57-
flow
58+
Flow
5859
....
59-
- A tupel of classes that together implement an SAML request-response pair between IDP and SP (and possible a discovery service). A class can be derived from Request, Response or (other), Check or Operation.
60-
- A flow for a solicited authentication consists of 4 classes:
61-
flow[0]: Operation (Handling a login flow such as discovery or WAYF - not implemented yet)
62-
flow[1]: Request (process the authentication request)
63-
flow[2]: Response (send the authentication response)
64-
flow[3]: Check (optional - can be None. E.g. check the response if a correct error status was raised when sending a broken response)
60+
* A tupel of classes that together implement an SAML request-response pair between IDP and SP (and possible other actors, such as a discovery service or IDP-proxy). A class can be derived from Request, Response (or other), Check or Operation.
61+
* A flow for a solicited authentication consists of 4 classes:
62+
63+
* flow[0]: Operation (Handling a login flow such as discovery or WAYF - not implemented yet)
64+
* flow[1]: Request (process the authentication request)
65+
* flow[2]: Response (send the authentication response)
66+
* flow[3]: Check (optional - can be None. E.g. check the response if a correct error status was raised when sending a broken response)
6567

6668
Check (and subclasses)
6769
.....
6870
- an optional class that is executed on receiving the SP's HTTP response(s) after the SAML response. If there are redirects it will be called for each response.
6971
- writes a structured test report to conv.test_output
7072
- It can check for expected errors, which do not cause an exception but in contrary are reported as success
7173

72-
interaction
74+
Interaction
7375
...........
7476
- An interaction automates a human interaction. It searches a response from a test target for some constants, and if
7577
there is a match, it will create a response suitable response.
7678

77-
(2) Simplefied Flow
79+
(2) Simplyfied Flow
7880
:::::::::::::::::::
7981

80-
The following pseudocdoe is an extract showing an overview of what is executed
81-
for test sp-00:
82+
The following pseudocode is an extract showing an overview of what is executed
83+
for test sp-00::
8284

83-
::
8485
do_sequence_and_test(self, oper, test):
8586
self.test_sequence(tests["pre"]) # currently no tests defined for sp_test
8687
for flow in oper:
@@ -97,8 +98,8 @@ for test sp-00:
9798
else:
9899
self.handle_result()
99100

100-
send_idp_response(req, resp):
101-
self.test_sequence(req.tests["post"]) # execute "post"-tests (request has "VerifyContent"-test built in; others from config)
101+
send_idp_response(req_flow, resp_flow):
102+
self.test_sequence(req_flow.tests["mid"]) # execute "mid"-tests (request has "VerifyContent"-test built in; others from config)
102103
# this line stands for a part that is a bit more involved .. see source
103104

104105
args.update(resp._response_args) # set userid, identity
@@ -129,3 +130,20 @@ for test sp-00:
129130
_txt = self.last_response.content
130131
if self.last_response.status_code >= 400:
131132
raise FatalError("Did not expected error")
133+
134+
(3) Status Reporting
135+
::::::::::::::::::::
136+
The proper reporting of results is at the core of saml2test. Several conditions
137+
must be considered:
138+
139+
#. An operation that was successful because the test target reports OK (e.g. HTTP 200)
140+
#. An operation that was successful because the test target reports NOK as expected, e.g. because of an invalid signature - HTTP 500 could be the correct response
141+
#. An errror in SAML2Test
142+
#. An errror in configuration of SAML2Test
143+
144+
Status values are defined in saml2test.check like this:
145+
INFORMATION = 0, OK = 1, WARNING = 2, ERROR = 3, CRITICAL = 4, INTERACTION = 5
146+
147+
148+
There are 2 targets to write output to:
149+
* Test_ouput is written to conv.test_ouput during the execution of the flows.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
author = "Roland Hedberg",
2727
author_email = "[email protected]",
2828
license="Apache 2.0",
29-
packages=["idp_test", "idp_test/package", "saml2test", "sp_test"],
29+
packages=["idp_test", "idp_test/package", "saml2test", "sp_test", "utility"],
3030
package_dir = {"": "src"},
3131
classifiers = [
3232
"Development Status :: 4 - Beta",

src/saml2test/check.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
INFORMATION = 0
1010
OK = 1
1111
WARNING = 2
12-
ERROR = 3
13-
CRITICAL = 4
12+
ERROR = 3 # an error condition in the test target
13+
CRITICAL = 4 # an error condition in the test driver
1414
INTERACTION = 5
1515

1616
STATUSCODE = ["INFORMATION", "OK", "WARNING", "ERROR", "CRITICAL",
@@ -124,8 +124,8 @@ def _func(self, conv):
124124

125125
class VerifyBadRequestResponse(ExpectedError):
126126
"""
127-
Verifies that the OP returned a 400 Bad Request response containing a
128-
Error message.
127+
Verifies that the test target returned a 400 Bad Request response
128+
containing a an error message.
129129
"""
130130
cid = "verify-bad-request-response"
131131
msg = "OP error"
@@ -138,7 +138,7 @@ def _func(self, conv):
138138
pass
139139
else:
140140
self._message = "Expected a 400 error message"
141-
self._status = CRITICAL
141+
self._status = ERROR
142142

143143
return res
144144

@@ -191,22 +191,22 @@ class Other(CriticalError):
191191
msg = "Other error"
192192

193193

194-
class CheckHTTPResponse(CriticalError):
194+
class CheckSpHttpResponseOK(Error):
195195
"""
196-
Checks that the HTTP response status is within the 200 or 300 range
196+
Checks that the SP's HTTP response status is within the 200 or 300 range
197197
"""
198-
cid = "check-http-response"
199-
msg = "OP error"
198+
cid = "check-sp-http-response-ok"
199+
msg = "SP error OK"
200200

201201
def _func(self, conv):
202202
_response = conv.last_response
203-
_content = conv.last_content
203+
_content = conv.last_response.content
204204

205205
res = {}
206206
if _response.status_code >= 400:
207207
self._status = self.status
208208
self._message = self.msg
209-
res["content"] = _content
209+
#res["content"] = _content #too big + charset converstion needed
210210
res["url"] = conv.position
211211
res["http_status"] = _response.status_code
212212

src/sp_test/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ def __init__(self, operations, check_factory):
7474
help="Path to the configuration file for the IdP")
7575
self._parser.add_argument("-t", dest="testpackage",
7676
help="Module describing tests")
77-
self._parser.add_argument('-v', dest='verbose', action='store_true',
78-
help="Print runtime information")
77+
#self._parser.add_argument('-v', dest='verbose', action='store_true',
78+
# help="Print runtime information") # unsused
7979
self._parser.add_argument("-Y", dest="pysamllog", action='store_true',
8080
help="Print PySAML2 logs")
8181
self._parser.add_argument("oper", nargs="?", help="Which test to run")

src/sp_test/base.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@
3737
class Conversation():
3838
def __init__(self, instance, config, interaction, json_config,
3939
check_factory, entity_id, msg_factory=None,
40-
features=None, verbose=False, constraints=None,
41-
expect_exception=None):
40+
features=None, constraints=None, # verbose=False,
41+
expect_exception=None, commandlineargs=None):
4242
self.instance = instance
4343
self._config = config
4444
self.test_output = []
4545
self.features = features
46-
self.verbose = verbose
46+
#self.verbose = verbose # removed (not used)
4747
self.check_factory = check_factory
4848
self.msg_factory = msg_factory
4949
self.expect_exception = expect_exception
@@ -70,7 +70,7 @@ def __init__(self, instance, config, interaction, json_config,
7070
self.position = ""
7171
self.response = None
7272
self.oper = None
73-
self.idp_constraints = constraints
73+
self.msg_constraints = constraints
7474
self.json_config = json_config
7575
self.start_page = json_config["start_page"]
7676

@@ -122,7 +122,7 @@ def test_sequence(self, sequence):
122122
kwargs = {}
123123
self.do_check(test, **kwargs)
124124
if test == ExpectedError:
125-
return False
125+
return False # TODO: return value is unused
126126
return True
127127

128128
def my_endpoints(self):
@@ -167,11 +167,6 @@ def handle_result(self, response=None):
167167
elif isinstance(response(), Check):
168168
self.do_check(response)
169169
else:
170-
# rhoerbe: I guess that this branch is never used, therefore
171-
# I am proposing this exception:
172-
#raise FatalError("can't use " + response.__class__.__name__ +
173-
# ", because it is not a subclass of 'Check'")
174-
#
175170
# A HTTP redirect or HTTP Post
176171
if 300 < self.last_response.status_code <= 303:
177172
self._redirect(self.last_response)
@@ -241,11 +236,11 @@ def _redirect(self, _response):
241236
break
242237
return url
243238

244-
def send_idp_response(self, req, resp):
239+
def send_idp_response(self, req_flow, resp_flow):
245240
"""
246-
:param req: The expected request
247-
:param resp: The response type to be used
248-
:return: A response
241+
:param req_flow: The flow to check the request
242+
:param resp_flow: The flow to prepare the response
243+
:return: The SP's HTTP response on receiving the SAML response
249244
"""
250245
# make sure I got the request I expected
251246
assert isinstance(self.saml_request.message, req._class)
@@ -257,37 +252,41 @@ def send_idp_response(self, req, resp):
257252

258253
# Pick information from the request that should be in the response
259254
args = self.instance.response_args(self.saml_request.message,
260-
[resp._binding])
261-
_mods = list(resp.__mro__[:])
255+
[resp_flow._binding])
256+
_mods = list(resp_flow.__mro__[:])
262257
_mods.reverse()
263258
for m in _mods:
264259
try:
265260
args.update(self.json_config["args"][m.__name__])
266261
except KeyError:
267262
pass
268263

269-
args.update(resp._response_args)
264+
args.update(resp_flow._response_args)
270265

271266
for param in ["identity", "userid"]:
272267
if param in self.json_config:
273268
args[param] = self.json_config[param]
274269

275-
if resp == ErrorResponse:
270+
if resp_flow == ErrorResponse:
276271
func = getattr(self.instance, "create_error_response")
277272
else:
278-
_op = camel2underscore.sub(r'_\1', req._class.c_tag).lower()
273+
_op = camel2underscore.sub(r'_\1', req_flow._class.c_tag).lower()
279274
func = getattr(self.instance, "create_%s_response" % _op)
280275

281276
# get from config which parts shall be signed
282277
sign = []
283278
for styp in ["sign_assertion", "sign_response"]:
284279
if styp in args:
285-
if args[styp].lower() == "always":
286-
sign.append(styp)
287-
del args[styp]
280+
try:
281+
if args[styp].lower() == "always":
282+
sign.append(styp)
283+
del args[styp]
284+
except (AttributeError, TypeError):
285+
raise AssertionError('config parameters "sign_assertion", '
286+
'"sign_response" must be of type string')
288287

289288
response = func(**args)
290-
response = resp(self).pre_processing(response)
289+
response = resp_flow(self).pre_processing(response)
291290
# and now for signing
292291
if sign:
293292
to_sign = []
@@ -315,21 +314,21 @@ def send_idp_response(self, req, resp):
315314
response = signed_instance_factory(response, self.instance.sec,
316315
to_sign)
317316

318-
info = self.instance.apply_binding(resp._binding, response,
317+
info = self.instance.apply_binding(resp_flow._binding, response,
319318
args["destination"],
320319
self.relay_state,
321-
"SAMLResponse", resp._sign)
320+
"SAMLResponse", resp_flow._sign)
322321

323-
if resp._binding == BINDING_HTTP_REDIRECT:
322+
if resp_flow._binding == BINDING_HTTP_REDIRECT:
324323
url = None
325324
for param, value in info["headers"]:
326325
if param == "Location":
327326
url = value
328327
break
329328
self.last_response = self.instance.send(url)
330-
elif resp._binding == BINDING_HTTP_POST:
331-
resp = base64.b64encode("%s" % response)
332-
info["data"] = urllib.urlencode({"SAMLResponse": resp,
329+
elif resp_flow._binding == BINDING_HTTP_POST:
330+
resp_flow = base64.b64encode("%s" % response)
331+
info["data"] = urllib.urlencode({"SAMLResponse": resp_flow,
333332
"RelayState": self.relay_state})
334333
info["method"] = "POST"
335334
info["headers"] = {
@@ -343,7 +342,7 @@ def do_flow(self, flow):
343342
Solicited or 'un-solicited' flows.
344343
345344
Solicited always starts with the Web client accessing a page.
346-
Un-solicited starts with the IDP sending something.
345+
Un-solicited starts with the IDP sending a SAMl Response.
347346
"""
348347
if len(flow) >= 3:
349348
self.wb_send_GET_startpage()
@@ -373,7 +372,7 @@ def do_sequence_and_tests(self, oper, tests=None):
373372
break
374373
except FatalError:
375374
raise
376-
except Exception:
375+
except Exception as err:
377376
#self.err_check("exception", err)
378377
raise
379378

0 commit comments

Comments
 (0)