Created
June 26, 2016 08:12
-
-
Save adammw/fc55fc2462f43472e8f1cc8c638e90b0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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