diff --git a/doc/howto.rst b/doc/howto.rst new file mode 100644 index 0000000..d09e6eb --- /dev/null +++ b/doc/howto.rst @@ -0,0 +1,296 @@ +.. _howto: + +How to use SAML2test +==================== + +:Release: |release| +:Date: |today| + +Before you can use SAML2test, you must get it installed. +If you have not done so yet, read :ref:`install`. + +When you want to test a SAML2 entity with this tool you need 3 things: + +* A configuration of the tool, an example can be found in tests/config_file.py +* A metadata file representing the tool +* A configuration file that describes how to interact with the entity. + The metadata for the entity is part of this file. More about this below. + +Tool configuration +:::::::::::::::::: + +This is a normal PySAML2 configuration file. You can have more than one and +then chose which one to use at run time by supplying the test script with +an argument. If no configuration is explicitly chosen the default name is +**config_file.py** . + +Interaction configuration file +:::::::::::::::::::::::::::::: + +The configuration is structured as a Python dictionary. +The keys are **entity_id**, **interaction** and **metadata**. + +entity_id +......... + +**entity_id** is really only necessary if there is more than one entity +represented in the metadata. If not provided and if the **metadata** only +describes one entity that entity's entityID is used. + +interaction +........... + +The really hard part is the **interaction** part. This is where the +the script is told how to fake that there is a human behind the keyboard. + +It consists of a lists of dictionaries with the keys: **matches**, +**page-type** and **control**. + +matches +------- + +**matches** is used to identify a page or a form within a page. +There are four different things that can be used to match the form: + +* url : The action url +* title : The title of the form, substring matching is used. +* content: Something in the form, again substring matching is used, and finally +* class: + +Normally the front-end will pick out the necessary information by +using a users interaction with the entity. If you are running this +directly from the prompt then you have to provide the information. +You can build this information by using the fact that the script will +dump any page it doesn't know what to do with. + +An example:: + + + { + "matches": { + "url": "http://localhost:8088/login", + "title": 'IDP test login' + }, + "page-type": "login", + "control": { + "type": "form", + "set": {"login": "roland", "password": "dianakra"} + } + } + +The action here is to set the control *login* to 'roland' and the control +*password* to 'dianakra' and then post the form. + +Or if the server uses HTTP Post binding:: + + { + "matches": { + "url": "http://localhost:8088/sso/redirect", + "title": "SAML 2.0 POST" + }, + "control": { + "type": "response", + "pick": {"form": {"action":"http://localhost:8088/acs"}} + } + }, + +Here the action is just to post the form, no information is added to the form. + +page-type +--------- + +**page-type** is used to mark the page as *login* or *user-consent*. +This is used in specific conversation where one or the other is expected +in certain circumstances. + +control +------- + +**control** specifies what the script should enter where and which button +to press. + +metadata +........ + +This is then the metadata for the entity to be tested. As noted previously +the metadata can actually describe more than one entity. In this case +the **entity_id** must be specified explicitly. + +Running the script +:::::::::::::::::: + +Script parameters:: + + $ saml2c.py --help + usage: saml2c.py [-h] [-d] [-v] [-C CA_CERTS] [-J JSON_CONFIG_FILE] [-m] [-l] + [-c SPCONFIG] + [oper] + + positional arguments: + oper Which test to run + + optional arguments: + -h, --help show this help message and exit + -d Print debug information + -v Print runtime information + -C CA_CERTS CA certs to use to verify HTTPS server certificates, if + HTTPS is used and no server CA certs are defined then + no cert verification will be done + -J JSON_CONFIG_FILE Script configuration + -m Return the SP metadata + -l List all the test flows as a JSON object + -c SPCONFIG Configuration file for the SP + + +To see what tests are available:: + + $ saml2c.py -l + [ + { + "id": "basic-authn", + "descr": "AuthnRequest using HTTP-redirect", + "name": "Absolute basic SAML2 AuthnRequest" + }, { + "id": "basic-authn-post", + "descr": "AuthnRequest using HTTP-POST", + "name": "Basic SAML2 AuthnRequest using HTTP POST" + }, { + "id": "log-in-out", + "descr": "AuthnRequest using HTTP-redirect followed by a logout", + "name": "Absolute basic SAML2 log in and out" + }, { + "id": "authn-assertion_id_request", + "descr": "AuthnRequest followed by an AssertionIDRequest", + "name": "AuthnRequest and then an AssertionIDRequest" + }, { + "id": "authn-authn_query", + "descr": "AuthnRequest followed by an AuthnQuery", + "name": "AuthnRequest and then an AuthnQuery" + } + ] + +A typical command would then be (reformated to be more readable):: + + $ saml2c.py -J localhost.json 'log-in-out' + { + "status": 1, + "tests": [ + { + "status": 1, + "id": "check-saml2int-metadata", + "name": "Checks that the Metadata follows the profile" + }, { + "status": 1, + "id": "check-http-response", + "name": "Checks that the HTTP response status is within the 200 or 300 range" + }, { + "status": 1, + "id": "check-http-response", + "name": "Checks that the HTTP response status is within the 200 or 300 range" + }, { + "status": 1, + "id": "check-http-response", + "name": "Checks that the HTTP response status is within the 200 or 300 range" + }, { + "status": 1, + "id": "check-saml2int-attributes", + "name": "Any elements exchanged via any SAML 2.0 messages, assertions, or metadata MUST contain a NameFormat of urn:oasis:names:tc:SAML:2.0:attrname-format:uri." + }, { + "status": 1, + "id": "verify-content", + "name": "Basic content verification class, does required and max/min checks" + }, { + "status": 1, + "id": "check-logout-support", + "name": "" + }, { + "status": 1, + "id": "verify-content", + "name": "Basic content verification class, does required and max/min checks" + }, { + "status": 1, + "id": "verify-logout", + "name": "" + } + ], + "id": "log-in-out" + } + +First you have the status for the whole test was '1', which is the same as OK, +for this test run. +The used status code are: + +0. INFORMATION +1. OK +2. WARNING +3. ERROR +4. CRITICAL +5. INTERACTION + +Then you get all the separate sub tests that has been run during the +conversation. + +If things go wrong you will get a trace log dump to stderr. +If all goes well but you still want to see all the interaction you can do:: + + $ saml2c.py -J localhost.json -d 'basic-authn' 2> tracelog + < same output as above > + $ cat tracelog + 0.017364 SAML Request: + http://localhost:8087/sp.xml + 0.036136 <-- REDIRECT TO: http://localhost:8088/login?came_from=%2Fsso%2Fredirect&key=331035cf0e26cdefc15759582e34994ac8e54971 + 0.040084 <-- CONTENT: + + + + IDP test login + + + + +
+

Login

+
+ + +

Please log in

+

+ To register it's quite simple: enter a login and a password +

+ +
+ + + +
+ +
+
+
+
+ +
+ +
+
+ +
+ + +
+ +
+ +
+ + + + 0.042697 >> login << + 0.042715 <-- FUNCTION: select_form + 0.042744 <-- ARGS: {u'set': {u'login': u'roland', u'password': u'dianakra'}, u'type': u'form', 'location': 'http://localhost:8088/login?came_from=%2Fsso%2Fredirect&key=331035cf0e26cdefc15759582e34994ac8e54971', '_trace_': , 'features': None} + 0.055864 <-- REDIRECT TO: http://localhost:8088/sso/redirect?id=zLvrjojPLLgbnDyq&key=331035cf0e26cdefc15759582e34994ac8e54971 + + ... and so on ... diff --git a/doc/index.rst b/doc/index.rst index 03fd715..054038a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,3 +1,5 @@ +.. _index: + .. SAML2test documentation master file, created by sphinx-quickstart on Sat Jan 19 11:38:19 2013. You can adapt this file completely to your liking, but it should at least @@ -6,12 +8,16 @@ Welcome to SAML2test's documentation! ===================================== +:Release: |release| +:Date: |today| + Contents: .. toctree:: - :maxdepth: 2 - + :maxdepth: 1 + howto + install Indices and tables ================== diff --git a/doc/install.rst b/doc/install.rst new file mode 100644 index 0000000..a19fd38 --- /dev/null +++ b/doc/install.rst @@ -0,0 +1,32 @@ +.. _install: + +Quick install guide +=================== + +Before you can use SAML2test, you'll need to get it installed. This guide +will guide you to a simple, minimal installation. + +Install SAML2test +----------------- + +For all this to work you need to have Python installed. +The development has been done using 2.7.2. +There is no 3.X version yet. + +Prerequisites +^^^^^^^^^^^^^ + +The big dependency is on pysaml2, which you for the time being must +get from github:: + + $ git clone https://github.com/rohe/pysaml2.git + +Quick build instructions +^^^^^^^^^^^^^^^^^^^^^^^^ + +Once you have installed all the necessary prerequisites a simple:: + + python setup.py install + +will install the basic code. + diff --git a/src/idp_test/__init__.py b/src/idp_test/__init__.py index bd27624..527e7a1 100644 --- a/src/idp_test/__init__.py +++ b/src/idp_test/__init__.py @@ -105,9 +105,6 @@ class SAML2client(object): help="CA certs to use to verify HTTPS server certificates, if HTTPS is used and no server CA certs are defined then no cert verification will be done") self._parser.add_argument('-J', dest="json_config_file", help="Script configuration") - self._parser.add_argument('-S', dest="sp_id", help="SP id") - self._parser.add_argument("-s", dest="list_sp_id", action="store_true", - help="List all the SP variants as a JSON object") self._parser.add_argument('-m', dest="metadata", action='store_true', help="Return the SP metadata") self._parser.add_argument("-l", dest="list", action="store_true", @@ -129,14 +126,7 @@ class SAML2client(object): def sp_configure(self, metadata_construction=False): sys.path.insert(0, ".") mod = import_module(self.args.spconfig) - if self.args.sp_id is None: - if len(mod.CONFIG) == 1: - self.args.sp_id = mod.CONFIG.keys()[0] - else: - raise Exception("SP id undefined") - - self.sp_config = SPConfig().load(mod.CONFIG[self.args.sp_id], - metadata_construction) + self.sp_config = SPConfig().load(mod.CONFIG, metadata_construction) def setup(self): self.json_config= self.json_config_file() @@ -144,7 +134,6 @@ class SAML2client(object): _jc = self.json_config self.interactions = _jc["interaction"] - self.entity_id = _jc["entity_id"] self.sp_configure() @@ -156,6 +145,16 @@ class SAML2client(object): metadata[0] = md self.sp_config.metadata = metadata + try: + self.entity_id = _jc["entity_id"] + # Verify its the correct metadata + assert self.entity_id in md.entity.keys() + except KeyError: + if len(md.entity.keys()) == 1: + self.entity_id = md.entity.keys()[0] + else: + raise Exception("Don't know which entity to talk to") + def test_summation(self, id): status = 0 for item in self.test_log: @@ -182,8 +181,6 @@ class SAML2client(object): if self.args.metadata: return self.make_meta() - elif self.args.list_sp_id: - return self.list_conf_id() elif self.args.list: return self.list_operations() else: diff --git a/src/idp_test/check.py b/src/idp_test/check.py index a1646d0..38ef389 100644 --- a/src/idp_test/check.py +++ b/src/idp_test/check.py @@ -328,6 +328,9 @@ class CheckSubjectNameIDFormat(Check): return res class CheckLogoutSupport(Check): + """ + Verifies that the tested entity supports single log out + """ id = "check-logout-support" msg = "Does not support logout" diff --git a/src/idp_test/saml2int.py b/src/idp_test/saml2int.py index f793832..31bdb66 100644 --- a/src/idp_test/saml2int.py +++ b/src/idp_test/saml2int.py @@ -113,7 +113,7 @@ OPERATIONS = { "sequence": [AuthnRequestPost], }, 'log-in-out': { - "name": 'Absolute basic SAML2 AuthnRequest', + "name": 'Absolute basic SAML2 log in and out', "descr": ('AuthnRequest using HTTP-redirect followed by a logout'), "sequence": [AuthnRequest, LogOutRequest], }, diff --git a/tests/config_file.py b/tests/config_file.py index 40da098..e3323e7 100755 --- a/tests/config_file.py +++ b/tests/config_file.py @@ -13,58 +13,55 @@ except Exception: #BASE = "http://lingon.ladok.umu.se:8087" BASE = "http://localhost:8087" -_CONFIG = { - "entityid" : "%s/sp.xml" % BASE, - "name" : "SAML2 test tool", - "description": "Simplest possible", - "service": { - "sp": { - "endpoints":{ - "assertion_consumer_service": [ - ("%s/acs/post" % BASE, BINDING_HTTP_POST), - ("%s/acs/redirect" % BASE, BINDING_HTTP_REDIRECT), - ("%s/acs/artifact" % BASE, BINDING_HTTP_ARTIFACT), - ("%s/ecp" % BASE, BINDING_PAOS) - ], - "single_logout_service": [ - ("%s/sls" % BASE, BINDING_SOAP) - ], - "artifact_resolution_service":[ - ("%s/ars" % BASE, BINDING_SOAP) - ], - "manage_name_id_service":[ - ("%s/mni" % BASE, BINDING_HTTP_POST), - ("%s/mni" % BASE, BINDING_HTTP_REDIRECT), - ("%s/mni" % BASE, BINDING_SOAP), - ("%s/acs/artifact" % BASE, BINDING_HTTP_ARTIFACT) - ] - } - } - }, - "key_file" : "keys/mykey.pem", - "cert_file" : "keys/mycert.pem", - "xmlsec_binary" : XMLSEC_BINARY, - "subject_data": "subject_data.db", - "accepted_time_diff": 60, - "attribute_map_dir" : "attributemaps", - "organization": { - "name": ("AB Exempel", "se"), - "display_name": ("AB Exempel", "se"), - "url": "http://www.example.org", - }, - "contact_person": [{ - "given_name": "Roland", - "sur_name": "Hedberg", - "telephone_number": "+46 70 100 0000", - "email_address": ["tech@eample.com", - "tech@example.org"], - "contact_type": "technical" - }, - ], - "secret": "0123456789", - "only_use_keys_in_metadata": False - } - CONFIG = { - "1": _CONFIG + "entityid" : "%s/sp.xml" % BASE, + "name" : "SAML2 test tool", + "description": "Simplest possible", + "service": { + "sp": { + "endpoints":{ + "assertion_consumer_service": [ + ("%s/acs/post" % BASE, BINDING_HTTP_POST), + ("%s/acs/redirect" % BASE, BINDING_HTTP_REDIRECT), + ("%s/acs/artifact" % BASE, BINDING_HTTP_ARTIFACT), + ("%s/ecp" % BASE, BINDING_PAOS) + ], + "single_logout_service": [ + ("%s/sls" % BASE, BINDING_SOAP) + ], + "artifact_resolution_service":[ + ("%s/ars" % BASE, BINDING_SOAP) + ], + "manage_name_id_service":[ + ("%s/mni" % BASE, BINDING_HTTP_POST), + ("%s/mni" % BASE, BINDING_HTTP_REDIRECT), + ("%s/mni" % BASE, BINDING_SOAP), + ("%s/acs/artifact" % BASE, BINDING_HTTP_ARTIFACT) + ] + } + } + }, + "key_file" : "keys/mykey.pem", + "cert_file" : "keys/mycert.pem", + "xmlsec_binary" : XMLSEC_BINARY, + "subject_data": "subject_data.db", + "accepted_time_diff": 60, + "attribute_map_dir" : "attributemaps", + "organization": { + "name": ("AB Exempel", "se"), + "display_name": ("AB Exempel", "se"), + "url": "http://www.example.org", + }, + "contact_person": [{ + "given_name": "Roland", + "sur_name": "Hedberg", + "telephone_number": "+46 70 100 0000", + "email_address": ["tech@eample.com", + "tech@example.org"], + "contact_type": "technical" + }, + ], + "secret": "0123456789", + "only_use_keys_in_metadata": False } + diff --git a/tests/localhost.py b/tests/localhost.py index 86cd43e..24b0e36 100755 --- a/tests/localhost.py +++ b/tests/localhost.py @@ -9,7 +9,6 @@ metadata = open("./idp/idp.xml").read() info = { "entity_id": "%s/idp.xml" % BASE, - "sp_config": "sp_local_conf.py", "interaction": [ { "matches": {