Skip to content

Instantly share code, notes, and snippets.

@adammw
Created June 26, 2016 08:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adammw/fc55fc2462f43472e8f1cc8c638e90b0 to your computer and use it in GitHub Desktop.
Save adammw/fc55fc2462f43472e8f1cc8c638e90b0 to your computer and use it in GitHub Desktop.
# Simple Blackboard Web Services Client
# Copyright (C) 2016, Adam Malcontenti-Wilson.
# Based on Blackboard Soap Web Services Python sample code, as licensed below
#
# Copyright (C) 2015, Blackboard Inc.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# -- Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# -- Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# -- Neither the name of Blackboard Inc. nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import sys
import suds
import random
from suds.client import Client
from suds.xsd.doctor import ImportDoctor, Import
from suds.wsse import *
from uuid import uuid1
from datetime import datetime
def generate_nonce(length=8):
"""Generate pseudorandom number."""
return ''.join([str(random.randint(0, 9)) for i in range(length)])
def createHeaders(action, username, password, endpoint):
"""Create the soap headers section of the XML to send to Blackboard Learn Web Service Endpoints"""
# Namespaces
xsd_ns = ('xsd', 'http://www.w3.org/2001/XMLSchema')
wsu_ns = ('wsu',"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd")
wsa_ns = ('wsa', 'http://schemas.xmlsoap.org/ws/2004/03/addressing')
# Set the action. This is a string passed to this funtion and corresponds to the method being called
# For example, if calling Context.WS.initialize(), this should be set to 'initialize'
wsa_action = Element('Action', ns=wsa_ns).setText(action)
# Each method requires a unique identifier. We are using Python's built-in uuid generation tool.
wsa_uuid = Element('MessageID', ns=wsa_ns).setText('uuid:' + str(uuid1()))
# Setting the replyTo address == to the SOAP role anonymous
wsa_address = Element('Address', ns=wsa_ns).setText('http://schemas.xmlsoap.org/ws/2004/03/addressing/role/anonymous')
wsa_replyTo = Element('ReplyTo', ns=wsa_ns).insert(wsa_address)
# Setting the To element to the endpoint being called
wsa_to = Element('To', ns=wsa_ns).setText(endpoint)
# Generate the WS_Security headers necessary to authenticate to Learn's Web Services
# To create a session, ContextWS.initialize() must first be called with username session and password no session.
# This will return a session Id, which then becomes the password for subsequent calls.
security = createWSSecurityHeader(username, password)
# Return the soapheaders that can be added to the soap call
return([wsa_action, wsa_uuid, wsa_replyTo, wsa_to, security])
def createWSSecurityHeader(username,password):
"""
Generate the WS-Security headers for making Blackboard Web Service calls.
SUDS comes with a WSSE header generation tool out of the box, but it does not offer
the flexibility needed to properly authenticate to the Blackboard SOAP-based services.
Thus, we are creating the necessary headers ourselves.
"""
# Namespaces
wsse = ('wsse', 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd')
wsu = ('wsu', 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd')
# Create Security Element
security = Element('Security', ns=wsse)
security.set('SOAP-ENV:mustUnderstand', '1')
# Create UsernameToken, Username/Pass Element
usernametoken = Element('UsernameToken', ns=wsse)
# Add the wsu namespace to the Username Token. This is necessary for the created date to be included.
# Also add a Security Token UUID to uniquely identify this username Token. This uses Python's built-in uuid generation tool.
usernametoken.set('xmlns:wsu', 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd')
usernametoken.set('wsu:Id', 'SecurityToken-' + str(uuid1()))
# Add the username token to the security header. This will always be 'session'
uname = Element('Username', ns=wsse).setText(username)
# Add the password element and set the type to 'PasswordText'.
# This will be nosession on the initialize() call, and the returned sessionID on subsequent calls.
passwd = Element('Password', ns=wsse).setText(password)
passwd.set('Type', 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText')
# Add a nonce element to further uniquely identify this message.
nonce = Element('Nonce', ns=wsse).setText(str(generate_nonce(24)))
# Add the current time in UTC format.
created = Element('Created', ns=wsu).setText(str(datetime.utcnow()))
# Add Username, Password, Nonce, and Created elements to UsernameToken element.
# Python inserts tags at the top, and Learn needs these in a specific order, so they are added in reverse order
usernametoken.insert(created)
usernametoken.insert(nonce)
usernametoken.insert(passwd)
usernametoken.insert(uname)
# Insert the usernametoken into the wsse:security tag
security.insert(usernametoken)
# Create the timestamp in the wsu namespace. Set a unique id for this timestamp using Python's built-in user generation tool.
timestamp = Element('Timestamp', ns=wsu)
timestamp.set('wsu:Id', 'Timestamp-' + str(uuid1()))
# Insert the timestamp into the wsse:security tag. This is done after usernametoken to insert before usernametoken in the subsequent XML
security.insert(timestamp)
# Return the security XML
return security
class BlackboardSoapClient:
def __init__(self, protocol, server, service_path):
self.url_header = protocol + "://" + server + "/" + service_path + "/"
self.services = {}
self.logger = logging.getLogger('blackboard.soapclient')
self.sessionId = 'nosession'
self.initializeSession()
def loadService(self, serviceName):
if not serviceName in self.services:
self.services[serviceName] = Client(self.getServiceEndpoint(serviceName) + '?wsdl', autoblend=True)
self.logger.debug("Loaded new service: %s\n%s", serviceName, self.services[serviceName])
if serviceName != 'Context.WS':
self.makeServiceCall(serviceName, 'initialize' + serviceName.replace('.',''), False)
return self.services[serviceName]
def createType(self, serviceName, typeName):
service = self.loadService(serviceName)
return service.factory.create(typeName)
def getServiceEndpoint(self, serviceName):
return self.url_header + serviceName
def makeServiceCall(self, serviceName, action, *args):
service = self.loadService(serviceName)
# Initialize headers and then call createHeaders to generate the soap headers with WSSE bits.
headers = createHeaders(action, 'session', self.sessionId, self.getServiceEndpoint(serviceName))
# Add Headers and WS-Security to client. Set port to default value, otherwise, you must add to service call
service.set_options(soapheaders=headers, port=serviceName + 'SOAP12port_https')
# Execute the service call
self.logger.debug("%s: %s%s", serviceName, action, args)
return service.service.__getattr__(action)(*args)
def initializeSession(self):
self.sessionId = self.makeServiceCall('Context.WS', 'initialize')
self.logger.debug("Session ID: %s", self.sessionId)
def login(self, userid, password, clientVendorId, clientProgramId, loginExtraInfo, expectedLifeSeconds):
return self.makeServiceCall('Context.WS', 'login', userid, password, clientVendorId, clientProgramId, loginExtraInfo, expectedLifeSeconds)
def logout(self):
self.makeServiceCall('Context.WS', 'logout')
self.sessionId = 'nosession'
if __name__ == '__main__':
# Set up logging. logging level is set to DEBUG on the suds tools in order to show you what's happening along the way.
# It will give you SOAP messages and responses, which will help you develop your own tool.
logging.basicConfig(level=logging.INFO)
logging.getLogger('suds.client').setLevel(logging.DEBUG)
# logging.getLogger('suds.transport').setLevel(logging.DEBUG)
# logging.getLogger('suds.xsd.schema').setLevel(logging.DEBUG)
# logging.getLogger('suds.wsdl').setLevel(logging.DEBUG)
logging.getLogger('blackboard.soapclient').setLevel(logging.DEBUG)
# Necessary system-setting for handling large complex WSDLs
sys.setrecursionlimit(10000)
# Create a client and login using a username and password
client = BlackboardSoapClient('https', 'ilearn.swin.edu.au', 'webapps/ws/services')
client.login('username', 'password', 'bb', 'blackboard', '', 3600)
# Retrieve course memeberships
courseMemberships = client.makeServiceCall('Context.WS', 'getMyMemberships')
courseIds = map(lambda course: course.externalId, courseMemberships)
courseFilter = client.createType('Course.WS', 'ns4:CourseFilter')
courseFilter.ids = courseIds
courseFilter.filterType = 3
print client.makeServiceCall('Course.WS', 'getCourse', courseFilter)
# Logout (invalidate session)
client.logout()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment