Actions

Plugins/Auth/WebServices

From Mahara Wiki

< Plugins‎ | Auth

Web Services support for Mahara

Artefact Webservices

Web Services plugin based support for Mahara.

The Web Services support is modelled on the webservices subsystem for Moodle and provides REST, XML-RPC, and SOAP alternatives for any registered function. The REST interface also supports JSON.

The idea is to provide a framework that makes it possible to develop additional APIs without having to do a great deal more than defining a function that exposes the required functionality, and the associated interface definition.

Installation Instructions

This requires the simple authentication module auth/webservice See https://gitorious.org/mahara-contrib/auth-webservice

To install you need to download both modules:

get https://gitorious.org/mahara-contrib/artefact-webservice/archive-tarball/master

and https://gitorious.org/mahara-contrib/auth-webservice/archive-tarball/master

Then do the following:

 cd /path/to/mahara/htdocs/auth
 tar -xzf /path/to/mahara-contrib-auth-webservice-master.tar.gz
 mv mahara-contrib-auth-webservice webservice
 cd /path/to/mahara/htdocs/artefact
 tar -xzf /path/to/mahara-contrib-artefact-webservice-master.tar.gz
 mv mahara-contrib-artefact-webservice webservice

You should now have the two necessary modules in place.

Now login as admin to Mahara, and go through the upgrade process to complete the install. In order to make the auth/webservice module available, you should add this as an authentication plugin for each Institution that requires access. Do this via the admin/users/institutions.php page.

It should be noted that it is completely hazardous to security if you don not SSL to protect the web services, as authentication details are transmitted openly - make sure you have HTTPS!

Configuration

The configuration interface can be found under Site administration for plugins - http://your.mahara.local.net/artefact/webservice/pluginconfig.php

Read on for more configuration details.

Testing

The testing framework and examples are based on simpletest unit tests. These are all located in the artefact/webservice/simpletest directory. To run these, you must switch user permissions to the web server user account eg:

 # for a debian based install
 cd /path/to/artefact/webservice/simpletest
 sudo -u www-data php testwebservice.php

This will run the tests in testwebservice.php which covers all the webservice API functions in artefact/webservice/serviceplugins/(user|group) that make up the core function interfaces.

If you do not switch user permissions then the testing will likely fail as the process will not have write permissions to the Mahara data directory.

Developing Functions for Services

Developing functions follow a plugin architecture where the following locations are swept on upgrade for artefact/webservice:

  • admin
  • local
  • api
  • artefact/webservice

Within each of these module locations there is a check for a subdirectory serviceplugins. The serviceplugins directory is then checked for further web service plugin directories. If we take the example of user and group, which exist as standard plugins in the artefact/webservice module, then the following directories exist:

  • artefact/webservice/serviceplugins/user
  • artefact/webservice/serviceplugins/group

Each plugin must have two files service.php and externallib.php.

externallib.php

This contains the description of the function interfaces, and the function to be called. These descriptions are used to determine what valid parameters can be passed, and is the basis for the data type checking that is performed automatically for import, and export parameter values. The interface description is also used to for the automatic API documentation generation that can be viewed in the web services configuration. The following is boiler plate code from the mahara_group_create_groups function API:

 class mahara_group_external extends external_api {
   ...
   /**
    * Returns description of method parameters
    * @return external_function_parameters
    */
   public static function create_groups_parameters() {
  
       $group_types = group_get_grouptypes();
       return new external_function_parameters(
           array(
               'groups' => new external_multiple_structure(
                   new external_single_structure(
                       array(
                           'name'            => new external_value(PARAM_RAW, 'Group name'),
                           'shortname'       => new external_value(PARAM_RAW, 'Group shortname for API only controlled groups', VALUE_OPTIONAL),
                           'description'     => new external_value(PARAM_NOTAGS, 'Group description'),
                           'institution'     => new external_value(PARAM_TEXT, 'Mahara institution - required for API controlled groups', VALUE_OPTIONAL),
                           'grouptype'       => new external_value(PARAM_ALPHANUMEXT, 'Group type: '.implode(',', $group_types)),
                           'jointype'        => new external_value(PARAM_ALPHANUMEXT, 'Join type - these are specific to group type - the complete set are: open, invite, request or controlled', VALUE_DEFAULT, 'controlled'),
                           'category'        => new external_value(PARAM_TEXT, 'Group category - the title of an existing group category'),
                           'public'          => new external_value(PARAM_INTEGER, 'Boolean 1/0 public group', VALUE_DEFAULT, '0'),
                           'usersautoadded'  => new external_value(PARAM_INTEGER, 'Boolean 1/0 for auto-adding users', VALUE_DEFAULT, '0'),
                           'members'         => new external_multiple_structure(
                                                           new external_single_structure(
                                                               array(
                                                                   'id' => new external_value(PARAM_NUMBER, 'member user Id', VALUE_OPTIONAL),
                                                                   'username' => new external_value(PARAM_RAW, 'member username', VALUE_OPTIONAL),
                                                                   'role' => new external_value(PARAM_ALPHANUMEXT, 'member role: admin, ')
                                                               ), 'Group membership')
                                                       ),
                           )
                   )
               )
           )
       );
   }
  
   /**
    * Create one or more group
    *
    * @param array $groups  An array of groups to create.
    * @return array An array of arrays
    */
   public static function create_groups($groups) {
       global $USER, $WEBSERVICE_INSTITUTION;
  
       // Do basic automatic PARAM checks on incoming data, using params description
       $params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));
       ...
    
       return $groupids;
   }
   
  /**
    * Returns description of method result value
    * @return external_description
    */
   public static function create_groups_returns() {
       return new external_multiple_structure(
           new external_single_structure(
               array(
                   'id'       => new external_value(PARAM_INT, 'group id'),
                   'name'     => new external_value(PARAM_RAW, 'group name'),
               )
           )
       );
   }
 ...
 }

service.php

This contains the description of functions to be added to the external_functions table, and gives the module author an opportunity to load up basic service groups for external_services. The following example is based on the mahara_group_external services:

 $functions = array(
  ...
     'mahara_group_create_groups' => array(
         'classname'   => 'mahara_group_external',
         'methodname'  => 'create_groups',
         'classpath'   => 'artefact/webservice/serviceplugins/group',
         'description' => 'Create groups',
         'type'        => 'write',
     ),
  ...
 );
 
 $services = array(
  ...
         'Simple Group Provisioning' => array(
                 'functions' => array ('mahara_group_create_groups', ... ),
                 'enabled'=>1,
                 'restrictedusers'=>1,
         ),
  ...
 );

Example Clients

Setting up web services clients is dead easy - as simple as shell scripting for the REST interface, eg:

echo "userids%5B0%5D=1&wsfunction=mahara_user_get_users_by_id&wstoken=591e9c08db612d2e4c9b2ea7634354c7" | POST -UsSe -H 'Host: mahara.local.net'  https://mahara.local.net/artefact/webservice/rest/server.php

PHP Clients

SOAP

Zend provide a convenient web services interface:

 // pull in the Zend libraries
 include 'Zend/Loader/Autoloader.php';
 Zend_Loader_Autoloader::autoload('Zend_Loader');
 
 // specfy the URL for looking up the WSDL interface definition
 $remotemaharaurl = 'https://your.mahara.local.net/artefact/webservice/soap/server.php';
 $token = '91525eca3e3cb11c8f5d94dc0b3c8d82';
 $wsdl = $remotemaharaurl. '?wstoken=' . $token.'&wsdl=1';
 
 //create the soap client instance
 $client = new Zend_Soap_Client($wsdl);
 
 //make the web service call
 try {
      $result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should be the admin account
      echo "result: ";
      var_dump($result);
 } catch (Exception $e) {
      echo "exception: ";
      var_dump($e);
 }

SOAP with WSSE

The SOAP Web Services Security Extension requires the addition of a specific SOAP packet header that contains the username and password. This is not automatically handled by the Zend web services classes, so the below example shows how to modify the HTTP client to get the new header included.

Note: there is a requirement to specify the wsservice query string parameter. Without this, the server would not know which services the user is trying to reach.

 include 'Zend/Loader/Autoloader.php';
 Zend_Loader_Autoloader::autoload('Zend_Loader');
 
 $remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/soap/simpleserver.php';
 $servicegroup = 'User Provisioning';
 $wsdl = $remotemaharaurl. '?wsservice=' . $servicegroup.'&wsdl=1';
 
 $userids = array(0=>1);
 
 //create the soap client instance
 class WSSoapClient extends Zend_Soap_Client_Common {
     private $username;
     private $password;
 
     /*Generates the WSSecurity header*/
     private function wssecurity_header() {
       $timestamp = gmdate('Y-m-d\TH:i:s\Z');
       $nonce = mt_rand();
       $passdigest = base64_encode(pack('H*', sha1(
                           pack('H*', $nonce) . pack('a*',$timestamp).
                           pack('a*',$this->password))));
       $auth = '
 <wsse:Security env:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.'.
 'org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
 <wsse:UsernameToken>
     <wsse:Username>'.$this->username.'</wsse:Username>
     <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-'.
 'wss-username-token-profile-1.0#PasswordText">'.$this->password.'</wsse:Password>
    </wsse:UsernameToken>
 </wsse:Security>
 ';
     $authvalues = new SoapVar($auth,XSD_ANYXML);
     $header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security", $authvalues, true);
     return $header;
   }
 
   /* set the username and password for the wsse header */
   public function setUsernameToken($username, $password) {
     $this->username = $username;
     $this->password = $password;
   }
 
   /* Overwrites the original method adding the security header */
   public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null) {
     return parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());
   }
 }
 
 $client = new Zend_Soap_Client($wsdl);
 $soapClient = new WSSoapClient(array($client, '_doRequest'), $wsdl, $client->getOptions());
 $soapClient->setUsernameToken('bean', 'blahblah');
 $client->setSoapClient($soapClient);
 
 
 //make the web service call
 try {
     $result = $client->mahara_user_get_users_by_id(array(0=>1)); // 1 should always be the admin user
     echo "result: ";
     var_dump($result);
 } catch (Exception $e) {
      echo "exception: ";
      var_dump($e);
 }

XML-RPC

The XML-RPC call is almost identical to the wstoken based SOAP call, except that it does not require WSDL discovery processing.

 include 'Zend/Loader/Autoloader.php';
 Zend_Loader_Autoloader::autoload('Zend_Loader');
 
 $remotemaharaurl = 'http://mahara.local.net/maharadev/artefact/webservice/xmlrpc/server.php';
 $token = '91525eca3e3cb11c8f5d94dc0b3c8d82';
 $serverurl = $remotemaharaurl. '?wstoken=' . $token;
 
 //create the xmlrpc client instance
 require_once 'Zend/XmlRpc/Client.php';
 $client = new Zend_XmlRpc_Client($serverurl);
   
 //make the web service call
 try {
    $result = $client->call('mahara_user_get_users_by_id', array(0=>1));
    var_dump($result);
 } catch (Exception $e) {
    var_dump($e);
 }

REST with simple authentication

The REST interface, is something akin to HTTP forms in that it requires parameters to be passed as either URI encoded query string or POST body parameters.

Note that the default output is the standard XML response as for XML-RPC, but this can be modified to emit JSON by adding the parameter "alt=json", or by setting the "Accept" HTTP-Header.

 include 'Zend/Loader/Autoloader.php';
 Zend_Loader_Autoloader::autoload('Zend_Loader');
 
 //set web service url server
 $serverurl = 'http://mahara.local.net/maharadev/artefact/webservice/rest/server.php?alt=json';
 $token = '91525eca3e3cb11c8f5d94dc0b3c8d82';
  
 $params = array(
     'userids' => array(1), // the params to passed to the function
     'wsfunction' => 'mahara_user_get_users_by_id',   // the function to be called
     'wstoken' => $token, //token need to be passed in the url
 );
  
 $client = new Zend_Http_Client($serverurl);
 //$client->setHeaders('Accept', 'application/jsonrequest');
   
 try {
     $client->setParameterPost($params);
     $response = $client->request('POST');
     var_dump (json_decode($response->getBody()));
 } catch (exception $exception) {
     var_dump ($exception);
 }

Core Function Interfaces

The base set of APIs are implmented in artefact/webservice/serviceplugins/.

These cover User, Favourite, and Group administration functions.