Using WSE3 services with Python

Like many companies, we use a number of Web Services at work. These are mainly used for our client side applications to talk back to things like SQL and other services, without going direct to them (and thus having a "security layer" of sorts). Having the ability to use these in some scripts/processes that you design can often be useful, as you have to play by the business logic.

In our case, our web services are .NET Web services using WSE3, to add on WS-Addressing and WS-Security features to them. These tie in easily to C# or other .NET languages, just by adding them into your Application. However, in a SysAdmin/DevOps role, you most likely aren't wanting to use a language like C#. You most likely want something like Python, Ruby or node.js; or you may be wanting to integrate these into something that you want to run on Linux or Solaris.

That was my thought. I wanted something in Python, to be able to run it from a few of our Linux boxes. On having a look around the internet, it seems that there is actually not much documentation on getting WSE3 (with WS-Addressing and WS-Security) web services to work in Python. So this article changes that.

In this article I will provide an example script which will show you how Python can talk to WSE3 scripts. It should hopefully point you in the right direction, that you need to go to achieve your goal.

What you need

Before we start, you will need to have available the following python libraries:

It will also help to have a small bit of knowledge about how SOAP works, but this is not required.

Our final script

We will start out, by showing the full script and then explaining each bit of how it works.

 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
#!/usr/bin/python

import logging
import random
import string

from suds import MethodNotFound
from suds.client import Client
from suds.wsse import Security, UsernameToken
from suds.sax.element import Element
from suds.sax.attribute import Attribute
from suds.xsd.sxbasic import Import

WEBSERVICE_URL = 'http://www.example.com/Webservice/Webservice.asmx'
NS_WSA = ('wsa', 'http://schemas.xmlsoap.org/ws/2004/08/addressing')
MUST_UNDERSTAND = Attribute('SOAP-ENV:mustUnderstand', 'true')

def main():
    logging.basicConfig(level=logging.INFO)
    logging.getLogger('suds.client').setLevel(logging.DEBUG)

    client = Client('%s?wsdl' % WEBSERVICE_URL)

    add_security(client, 'DOMAIN\User', 'Password')
    add_addressing(client, WEBSERVICE_URL)
    method = get_method(client, 'method')

    print method()

def add_security(client, user, passwd):
    sec = Security()
    token = UsernameToken(user, passwd)
    token.setnonce()
    token.setcreated()
    sec.tokens.append(token)
    client.set_options(wsse=sec)

def add_addressing(client, webservice_url):
    headers = []

    addr = Element('Address', ns=NS_WSA).setText('http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous')

    headers.append(Element('Element').addPrefix(p='SOAP-ENC', u='http://www.w3.org/2003/05/soap-encoding'))
    headers.append(Element('ReplyTo', ns=NS_WSA).insert(addr).append(MUST_UNDERSTAND))
    headers.append(Element('To', ns=NS_WSA).setText(webservice_url).append(MUST_UNDERSTAND))
    headers.append(addr)
    headers.append(Element('MessageID', ns=NS_WSA).setText('urn:uuid:%s' % generate_messageid()))

    client.set_options(soapheaders=headers)

def get_method(client, method):
    try:
        m = getattr(client.service, method)
        action = client.wsdl.services[0].ports[0].methods[method].soap.action
        action = action.replace('"', '')
    except MethodNotFound:
        return None

    action_header = Element('Action', ns=NS_WSA).setText(action)
    client.options.soapheaders.append(action_header)

    return m

def generate_messageid():
    fmt = 'xxxxxxxx-xxxxx'
    resp = ''

    for c in fmt:
        if c == '-':
            resp += c
        else:
            resp += string.hexdigits[random.randrange(16)]

    return resp

if __name__ == '__main__':
    main()

How the script works in detail

Now that we have seen the full script, we will go thru it, and explain what it does.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import logging
import random
import string

from suds import MethodNotFound
from suds.client import Client
from suds.wsse import Security, UsernameToken
from suds.sax.element import Element
from suds.sax.attribute import Attribute
from suds.xsd.sxbasic import Import

First we start by including some libraries we are going to use. We use the random library for generating a random message ID for our SOAP message. The includes from suds.* are for the SOAP library we will be using.

We have also included the logging library, which will enable us to be able to see some debug messages should we wish to.

1
2
3
WEBSERVICE_URL = 'http://www.example.com/Webservice/Webservice.asmx'
NS_WSA = ('wsa', 'http://schemas.xmlsoap.org/ws/2004/08/addressing')
MUST_UNDERSTAND = Attribute('SOAP-ENV:mustUnderstand', 'true')

Next, we define our webservice url (WEBSERVICE_URL). This should be the full path to the endpoint. This should not be the URL to it's WSDL.

We also define some attributes for our XML Elements that we will needed further down in our script. NS_WSA is the namespace we are defining for WS-Addressing. The MUST_UNDERSTAND variable is used to make Elements which the Web Service must understand and process.

1
2
3
def main():
    logging.basicConfig(level=logging.INFO)
    logging.getLogger('suds.client').setLevel(logging.DEBUG)

This is our main() function, that will be called when we start the script. The first two lines, will provide our debug output for running the script. These can be commented out afterwards should you want to.

1
client = Client('%s?wsdl' % WEBSERVICE_URL)

We next create an instance of the suds.client.Client object, which will be used for connecting and inspecting our web service.

1
2
3
4
5
add_security(client, 'DOMAIN\User', 'Password')
add_addressing(client, WEBSERVICE_URL)
method = get_method(client, 'method')

print method()

The last bit of our main() function, will be where we call our other functions to add in support for WS-Addressing and WS-Security. Then the last bit will be finalising the method we are going to call, and calling the method and printing out the results.

You would change/set the DOMAINUser and Password, as well as the method here, so it runs with the right credentials and the correct method of the web service.

1
2
3
4
5
6
7
def add_security(client, user, passwd):
    sec = Security()
    token = UsernameToken(user, passwd)
    token.setnonce()
    token.setcreated()
    sec.tokens.append(token)
    client.set_options(wsse=sec)

This is our add_security() function which will take care of implementing WS-Security into our request. Luckily for us, WS-Security support is available in Suds already, so we just need to add it to our client object.

The token we add is our username and password (username is prefixed with the Domain when it is sent to the function). We also set a "Nonce" and the created date and time. These together help to prevent replay attacks with the SOAP request.

Finally we add our token to our security object, and add our security object, into our client object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def add_addressing(client, webservice_url):
    headers = []

    addr = Element('Address', ns=NS_WSA).setText('http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous')

    headers.append(Element('Element').addPrefix(p='SOAP-ENC', u='http://www.w3.org/2003/05/soap-encoding'))
    headers.append(Element('ReplyTo', ns=NS_WSA).insert(addr).append(MUST_UNDERSTAND))
    headers.append(Element('To', ns=NS_WSA).setText(webservice_url).append(MUST_UNDERSTAND))
    headers.append(addr)
    headers.append(Element('MessageID', ns=NS_WSA).setText('urn:uuid:%s' % generate_messageid()))

    client.set_options(soapheaders=headers)

The next function we call in our script, is the add_addressing() function. This function adds most required headers for WS-Addressing. There is one that is added the get_method() function, but we will cover that shortly.

First we start by defining an "Address" Element, which is a static reference to the WS-Addressing specification. Note we also set the namespace to the NS_WSA variable we created befoer. After that, we create a generic Element tag, which is used to define the SOAP Encoding we are using.

Next are the two most important elements we will add to the header. The ReplyTo Element is added, with reference to the Address element, and the mustUnderstand attribute. The ReplyTo element defines the endpoint for the reply to the web service. The To Element is where we define the address of the web service we are calling.

We finish up by adding the Address element into the headers, and defining a unique MessageID for the message we are able to send. We then add these headers into our client object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def get_method(client, method):
    try:
        m = getattr(client.service, method)
        action = client.wsdl.services[0].ports[0].methods[method].soap.action
        action = action.replace('"', '')
    except MethodNotFound:
        return None

    action_header = Element('Action', ns=NS_WSA).setText(action)
    client.options.soapheaders.append(action_header)

    return m

This function, get_method(), is used to be able to find the action path of the method we want to call, and add this int our SOAP headers.

We first look up to see whether the method we have been given, is a valid method that exists when the WSDL file is introspected. If it is, we store this in m, for returning back later.

Before finishing up however, we look in the WSDL definition for the service, and get the SOAP action attribute for the given method. We remove and bad characters and add this into our SOAP Headers as the Action element. This tells WS-Addressing the function we want to call to do our work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def generate_messageid():
    fmt = 'xxxxxxxx-xxxxx'
    resp = ''

    for c in fmt:
        if c == '-':
            resp += c
        else:
            resp += string.hexdigits[random.randrange(16)]

    return resp

The generate_messageid() function is used by add_addressing(), to generate a Unique and random message ID. This function simple look over each character in our defined format (fmt), and generates a unique hex character to be returned. Literal dashes (-) in the format string are ignored.

1
2
if __name__ == '__main__':
    main()

To finish off, this is stock standard python. If this script is invoked from the command line, the built-in variable name will be set to main. We simply say here, that if it is invoked from the command line, run our main() function.

Now, if you have updated some of the variables where needed in the above, you will now be able to run the script, and it should print our the resulting dataset from your web service. If you get any errors, have a look and see if you can work out what may be causing it. Do check that it's something as simple as maybe the wrong username or password tho.

Where to from here?

Now that you have the script, you will most likely want to use what you have learnt here, to form this into a library of functions, so you can use it in your application. And get the values from somewhere else, than what is hardcoded in the script.

You will also want to have a play around with calling method's with parameters and also seeing what happens when you start to need to use more complex data types that the web service method defines. But you should be able to handle this, as Suds includes this introspection in it's API support.

Should you have any questions, feel free to contact me, and i'll see what I can do!

I should also thank the below articles/posts on websites for helping, as without them, I may have had no clue to some bits of this.

Published: 12 March 2012 # — Tags: python, work