MDL-21579 "Implement session token for embedded application" implemented a second...
authorJamie Pratt <me@jamiep.org>
Wed, 28 Apr 2010 13:16:58 +0000 (13:16 +0000)
committerJamie Pratt <me@jamiep.org>
Wed, 28 Apr 2010 13:16:58 +0000 (13:16 +0000)
18 files changed:
admin/cron.php
lang/en/error.php
lang/en/webservice.php
lib/externallib.php
lib/sessionlib.php
webservice/amf/locallib.php
webservice/amf/server.php
webservice/amf/simpleserver.php
webservice/lib.php
webservice/rest/locallib.php
webservice/rest/server.php
webservice/rest/simpleserver.php
webservice/soap/locallib.php
webservice/soap/server.php
webservice/soap/simpleserver.php
webservice/xmlrpc/locallib.php
webservice/xmlrpc/server.php
webservice/xmlrpc/simpleserver.php

index eca2939..f845a00 100644 (file)
     $fs = get_file_storage();
     $fs->cron();
 
+    //cleanup old session linked tokens
+    //deletes the session linked tokens that are over a day old.
+    mtrace("Deleting session linked tokens more than one day old...", '');
+    $DB->delete_records_select('external_tokens', 'lastaccess < {onedayago} AND tokentype = {tokentype}', 
+                    array('onedayago' => time() - DAYSECS, 'tokentype' => EXTERNAL_TOKEN_EMBEDDED));
+    
     // run any customized cronjobs, if any
     if ($locals = get_plugin_list('local')) {
         mtrace('Processing customized cron scripts ...', '');
index 97a4c6a..08afe1b 100755 (executable)
@@ -442,7 +442,7 @@ $string['tagnotfound'] = 'The specified tag was not found in the database';
 $string['targetdatabasenotempty'] = 'The target database is not empty. Transfer aborted for safety reasons.';
 $string['themenotinstall'] = 'This theme is not installed!';
 $string['TODO'] = 'TODO';
-$string['tokenalreadyexist'] = 'The generated token already exists, try again.';
+$string['tokengenerationfailed'] = 'Cannot generate a new token.';
 $string['transactionvoid'] = 'Transaction cannot be voided because it has already been voided';
 $string['unenrolerror'] = 'An error occurred while trying to unenrol that person';
 $string['unicodeupgradeerror'] = 'Sorry, but your database is not already in Unicode, and this version of Moodle is not able to migrate your database to Unicode.  Please upgrade to Moodle 1.7.x first and perform the Unicode migration from the Admin page.  After that is done you should be able to migrate to Moodle {$a}';
index 1bb8d1a..f9a4ba6 100644 (file)
@@ -96,6 +96,7 @@ $string['invalidextresponse'] = 'Invalid external api response: {$a}';
 $string['invalidiptoken'] = 'Invalid token - your IP is not supported';
 $string['invalidtimedtoken'] = 'Invalid token - token expired';
 $string['invalidtoken'] = 'Invalid token - token not found';
+$string['invalidtokensession'] = 'Invalid session based token - session not found or expired';
 $string['iprestriction'] = 'IP restriction';
 $string['key'] = 'Key';
 $string['keyshelp'] = 'The keys are used to access your Moodle account from external applications.';
index 15247fa..66b0d7a 100644 (file)
@@ -453,6 +453,9 @@ function external_generate_token($tokentype, $serviceorid, $userid, $contextorid
     }
     $newtoken->tokentype = $tokentype;
     $newtoken->userid = $userid;
+    if ($tokentype == EXTERNAL_TOKEN_EMBEDDED){
+        $newtoken->sid = session_id();
+    }
 
     $newtoken->contextid = $context->id;
     $newtoken->creatorid = $USER->id;
@@ -463,4 +466,18 @@ function external_generate_token($tokentype, $serviceorid, $userid, $contextorid
     }
     $DB->insert_record('external_tokens', $newtoken);
     return $newtoken->token;
+}
+/**
+ * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate 
+ * with the Moodle server through web services. The token is linked to the current session for the current page request.
+ * It is expected this will be called in the script generating the html page that is embedding the client app and that the
+ * returned token will be somehow passed into the client app being embedded in the page.
+ * @param string $servicename name of the web service. Service name as defined in db/services.php
+ * @param int $context context within which the web service can operate.
+ * @return int returns token id.
+ */
+function external_create_service_token($servicename, $context){
+    global $USER, $DB;
+    $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST);
+    return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0);
 }
\ No newline at end of file
index 697dd4f..31c9f2c 100644 (file)
@@ -74,6 +74,13 @@ interface moodle_session {
      * @return void
      */
     public function write_close();
+    
+    /**
+     * Check for existing session with id $sid
+     * @param unknown_type $sid
+     * @return boolean true if session found.
+     */
+    public function session_exists($sid);
 }
 
 /**
@@ -145,8 +152,10 @@ abstract class session_stub implements moodle_session {
      * Terminates active moodle session
      */
     public function terminate_current() {
-        global $CFG, $SESSION, $USER;
+        global $CFG, $SESSION, $USER, $DB;
 
+        $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
+        
         if (NO_MOODLE_COOKIES) {
             return;
         }
@@ -349,6 +358,18 @@ class legacy_file_session extends session_stub {
         }
         ini_set('session.save_path', $CFG->dataroot .'/sessions');
     }
+    /**
+     * Check for existing session with id $sid
+     * @param unknown_type $sid
+     * @return boolean true if session found.
+     */
+    public function session_exists($sid){
+        $sid = clean_param($sid, PARAM_FILE);
+        $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
+        return file_exists($sessionfile);
+    }
+    
+
 }
 
 /**
@@ -376,7 +397,20 @@ class database_session extends session_stub {
             }
         }
     }
-
+    
+    public function session_exists($sid){
+        global $CFG;
+        try {
+            $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
+            $params = array(time() + $CFG->sessiontimeout, $sid, 0);
+            
+            return $this->database->record_exists_sql($sql, $params);
+        } catch (dml_exception $ex) {
+            error_log('Error checking existance of database session');
+            return false;
+        }
+    }
+    
     protected function init_session_storage() {
         global $CFG;
 
index 1e92b54..ba5c513 100644 (file)
@@ -47,26 +47,28 @@ class invalid_return_value_exception extends moodle_exception {
 class webservice_amf_server extends webservice_zend_server {
     /**
      * Contructor
-     * @param bool $simple use simple authentication
+     * @param integer $authmethod authentication method - one of WEBSERVICE_AUTHMETHOD_*
      */
-    public function __construct($simple) {
+    public function __construct($authmethod) {
         require_once 'Zend/Amf/Server.php';
-        parent::__construct($simple, 'Zend_Amf_Server');
+        parent::__construct($authmethod, 'Zend_Amf_Server');
         $this->wsname = 'amf';
     }
     protected function init_service_class(){
-       parent::init_service_class();
+        parent::init_service_class();
         //allow access to data about methods available.
         $this->zend_server->setClass( "MethodDescriptor" );
         MethodDescriptor::$classnametointrospect = $this->service_class;
     }
     
     protected function service_class_method_body($function, $params){
-       $params = "webservice_amf_server::cast_objects_to_array($params)";
-       $externallibcall = $function->classname.'::'.$function->methodname.'('.$params.')';
-       $descriptionmethod = $function->methodname.'_returns()';
-       $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
-       return
+        if ($params){
+            $params = "webservice_amf_server::cast_objects_to_array($params)";
+        }
+        $externallibcall = $function->classname.'::'.$function->methodname.'('.$params.')';
+        $descriptionmethod = $function->methodname.'_returns()';
+        $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
+        return
 '        return webservice_amf_server::validate_and_cast_values('.$callforreturnvaluedesc.', '.$externallibcall.');';
     }
     /**
@@ -80,9 +82,9 @@ class webservice_amf_server extends webservice_zend_server {
      * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
      */
     public static function validate_and_cast_values($description, $value) {
-       if (is_null($description)){
-               return;
-       }
+        if (is_null($description)){
+            return;
+        }
         if ($description instanceof external_value) {
             if (is_array($value) or is_object($value)) {
                 throw new invalid_return_value_exception('Scalar type expected, array or object received.');
@@ -136,27 +138,27 @@ class webservice_amf_server extends webservice_zend_server {
             throw new invalid_return_value_exception('Invalid external api description.');
         }
     }    
-       /**
-        * Recursive function to recurse down into a complex variable and convert all
-        * objects to arrays. Doesn't recurse down into objects or cast objects other than stdClass
-        * which is represented in Flash / Flex as an object. 
-        * @param mixed $params value to cast
-        * @return mixed Cast value
-        */
-       public static function cast_objects_to_array($params){
-               if ($params instanceof stdClass){
-                       $params = (array)$params;
-               }
-               if (is_array($params)){
-                       $toreturn = array();
-                       foreach ($params as $key=> $param){
-                               $toreturn[$key] = self::cast_objects_to_array($param);
-                       }
-                       return $toreturn;
-               } else {
-                       return $params;
-               }
-       }
+    /**
+     * Recursive function to recurse down into a complex variable and convert all
+     * objects to arrays. Doesn't recurse down into objects or cast objects other than stdClass
+     * which is represented in Flash / Flex as an object. 
+     * @param mixed $params value to cast
+     * @return mixed Cast value
+     */
+    public static function cast_objects_to_array($params){
+        if ($params instanceof stdClass){
+            $params = (array)$params;
+        }
+        if (is_array($params)){
+            $toreturn = array();
+            foreach ($params as $key=> $param){
+                $toreturn[$key] = self::cast_objects_to_array($param);
+            }
+            return $toreturn;
+        } else {
+            return $params;
+        }
+    }
     /**
      * Set up zend service class
      * @return void
index 76bc954..2db5bca 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('amf')) {
     die;
 }
 
-$server = new webservice_amf_server(false);
+$server = new webservice_amf_server(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN);
 $server->run();
 die;
 
index c00d984..8ae9e65 100644 (file)
@@ -41,7 +41,7 @@ if (!webservice_protocol_is_enabled('amf')) {
     die;
 }
 
-$server = new webservice_amf_server(true);
+$server = new webservice_amf_server(WEBSERVICE_AUTHMETHOD_USERNAME);
 $server->run();
 die;
 
index bc18893..3619e18 100644 (file)
 
 require_once($CFG->libdir.'/externallib.php');
 
+define('WEBSERVICE_AUTHMETHOD_USERNAME', 0);
+define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1);
+define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2);
+
 /**
  * Exception indicating access control problem in web service call
  * @author Petr Skoda (skodak)
@@ -102,8 +106,8 @@ abstract class webservice_server implements webservice_server_interface {
     /** @property int $userid the local user */
     protected $userid = null;
 
-    /** @property bool $simple true if simple auth used */
-    protected $simple;
+    /** @property integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* */
+    protected $authmethod;
 
     /** @property string $token authentication token*/
     protected $token = null;
@@ -114,6 +118,15 @@ abstract class webservice_server implements webservice_server_interface {
     /** @property int restrict call to one service id*/
     protected $restricted_serviceid = null;
 
+    /**
+     * Contructor
+     * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* 
+     */
+    public function __construct($authmethod) {
+        $this->authmethod = $authmethod;
+    }    
+    
+    
     /**
      * Authenticate user using username+password or token.
      * This function sets up $USER global.
@@ -129,7 +142,7 @@ abstract class webservice_server implements webservice_server_interface {
             throw new coding_exception('Cookies must be disabled in WS servers!');
         }
 
-        if ($this->simple) {
+        if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
 
             //we check that authentication plugin is enabled
             //it is only required by simple authentication
@@ -159,41 +172,60 @@ abstract class webservice_server implements webservice_server_interface {
 
             $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0), '*', MUST_EXIST);
 
+        } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN){
+            $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT);
         } else {
-            if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>EXTERNAL_TOKEN_PERMANENT))) {
-                // log failed login attempts
-                add_to_log(1, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->token. " - ".getremoteaddr() , 0);
-                throw new webservice_access_exception(get_string('invalidtoken', 'webservice'));
-            }
-
-            if ($token->validuntil and $token->validuntil < time()) {
-                throw new webservice_access_exception(get_string('invalidtimedtoken', 'webservice'));
-            }
-
-            if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
-                add_to_log(1, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0);
-                throw new webservice_access_exception(get_string('invalidiptoken', 'webservice'));
-            }
-
-            $this->restricted_context = get_context_instance_by_id($token->contextid);
-            $this->restricted_serviceid = $token->externalserviceid;
-
-            $user = $DB->get_record('user', array('id'=>$token->userid, 'deleted'=>0), '*', MUST_EXIST);
-
-            // log token access
-            $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id));
+            $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED);
         }
-
+        
         // now fake user login, the session is completely empty too
         session_set_user($user);
         $this->userid = $user->id;
 
-        if (!has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
+        if ($this->authmethod != EXTERNAL_TOKEN_EMBEDDED && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
             throw new webservice_access_exception(get_string('accessnotallowed', 'webservice'));
         }
 
         external_api::set_context_restriction($this->restricted_context);
     }
+    
+    protected function authenticate_by_token($tokentype){
+        global $DB;
+        if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype))) {
+            // log failed login attempts
+            add_to_log(1, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->token. " - ".getremoteaddr() , 0);
+            throw new webservice_access_exception(get_string('invalidtoken', 'webservice'));
+        }
+    
+        if ($token->validuntil and $token->validuntil < time()) {
+            $DB->delete_records('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype));
+            throw new webservice_access_exception(get_string('invalidtimedtoken', 'webservice'));
+        }
+        
+        if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type
+            $session = session_get_instance();
+            if (!$session->session_exists($token->sid)){
+                $DB->delete_records('external_tokens', array('sid'=>$token->sid));
+                throw new webservice_access_exception(get_string('invalidtokensession', 'webservice'));
+            }
+        }
+
+        if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
+            add_to_log(1, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0);
+            throw new webservice_access_exception(get_string('invalidiptoken', 'webservice'));
+        }
+
+        $this->restricted_context = get_context_instance_by_id($token->contextid);
+        $this->restricted_serviceid = $token->externalserviceid;
+
+        $user = $DB->get_record('user', array('id'=>$token->userid, 'deleted'=>0), '*', MUST_EXIST);
+
+        // log token access
+        $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id));
+        
+        return $user;
+        
+    }
 }
 
 /**
@@ -214,10 +246,10 @@ abstract class webservice_zend_server extends webservice_server {
 
     /**
      * Contructor
-     * @param bool $simple use simple authentication
+     * @param integer $authmethod authentication method - one of WEBSERVICE_AUTHMETHOD_*
      */
-    public function __construct($simple, $zend_class) {
-        $this->simple = $simple;
+    public function __construct($authmethod, $zend_class) {
+        parent::__construct($authmethod);
         $this->zend_class = $zend_class;
     }
 
@@ -483,7 +515,7 @@ class '.$classname.' {
      */
     protected function service_class_method_body($function, $params){
         $descriptionmethod = $function->methodname.'_returns()';
-       $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
+        $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
         return '    if ('.$callforreturnvaluedesc.' == null)  {
                         return null;
                     }
@@ -506,7 +538,7 @@ class '.$classname.' {
      * @return void
      */
     protected function parse_request() {
-        if ($this->simple) {
+        if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
             //note: some clients have problems with entity encoding :-(
             if (isset($_REQUEST['wsusername'])) {
                 $this->username = $_REQUEST['wsusername'];
@@ -570,7 +602,7 @@ class '.$classname.' {
      * @return void
      */
     protected function session_cleanup($exception=null) {
-        if ($this->simple) {
+        if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
             // nothing needs to be done, there is no persistent session
         } else {
             // close emulated session if used
@@ -598,14 +630,6 @@ abstract class webservice_base_server extends webservice_server {
     /** @property mixed $returns function return value */
     protected $returns = null;
 
-    /**
-     * Contructor
-     * @param bool $simple use simple authentication
-     */
-    public function __construct($simple) {
-        $this->simple = $simple;
-    }
-
     /**
      * This method parses the request input, it needs to get:
      *  1/ user authentication - username+password or token
index 92e9b5b..ba5b5ed 100644 (file)
@@ -33,8 +33,8 @@ class webservice_rest_server extends webservice_base_server {
     /**
      * Contructor
      */
-    public function __construct($simple) {
-        parent::__construct($simple);
+    public function __construct($authmethod) {
+        parent::__construct($authmethod);
         $this->wsname = 'rest';
     }
 
index 8bace32..de50f26 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('rest')) {
     die;
 }
 
-$server = new webservice_rest_server(false);
+$server = new webservice_rest_server(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN);
 $server->run();
 die;
 
index 9fb495f..f35340f 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('rest')) {
     die;
 }
 
-$server = new webservice_rest_server(true);
+$server = new webservice_rest_server(WEBSERVICE_AUTHMETHOD_USERNAME);
 $server->run();
 die;
 
index 6915488..b7f7d0b 100644 (file)
@@ -34,16 +34,16 @@ class webservice_soap_server extends webservice_zend_server {
      * Contructor
      * @param bool $simple use simple authentication
      */
-    public function __construct($simple) {
+    public function __construct($authmethod) {
          // must not cache wsdl - the list of functions is created on the fly
         ini_set('soap.wsdl_cache_enabled', '0');
         require_once 'Zend/Soap/Server.php';
         require_once 'Zend/Soap/AutoDiscover.php';
 
         if (optional_param('wsdl', 0, PARAM_BOOL)) {
-            parent::__construct($simple, 'Zend_Soap_AutoDiscover');
+            parent::__construct($authmethod, 'Zend_Soap_AutoDiscover');
         } else {
-            parent::__construct($simple, 'Zend_Soap_Server');
+            parent::__construct($authmethod, 'Zend_Soap_Server');
         }
         $this->wsname = 'soap';
     }
index 4db0776..e0ee89f 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('soap')) {
     die;
 }
 
-$server = new webservice_soap_server(false);
+$server = new webservice_soap_server(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN);
 $server->run();
 die;
 
index a37e365..9142a2c 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('soap')) {
     die;
 }
 
-$server = new webservice_soap_server(true);
+$server = new webservice_soap_server(WEBSERVICE_AUTHMETHOD_USERNAME);
 $server->run();
 die;
 
index 4489f0c..0fbcee7 100644 (file)
@@ -32,11 +32,11 @@ require_once("$CFG->dirroot/webservice/lib.php");
 class webservice_xmlrpc_server extends webservice_zend_server {
     /**
      * Contructor
-     * @param bool $simple use simple authentication
+     * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* 
      */
-    public function __construct($simple) {
+    public function __construct($authmethod) {
         require_once 'Zend/XmlRpc/Server.php';
-        parent::__construct($simple, 'Zend_XmlRpc_Server');
+        parent::__construct($authmethod, 'Zend_XmlRpc_Server');
         $this->wsname = 'xmlrpc';
     }
 
index 91038d4..ae70ca0 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('xmlrpc')) {
     die;
 }
 
-$server = new webservice_xmlrpc_server(false);
+$server = new webservice_xmlrpc_server(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN);
 $server->run();
 die;
 
index f5c6a7a..6f35475 100644 (file)
@@ -34,7 +34,7 @@ if (!webservice_protocol_is_enabled('xmlrpc')) {
     die;
 }
 
-$server = new webservice_xmlrpc_server(true);
+$server = new webservice_xmlrpc_server(WEBSERVICE_AUTHMETHOD_USERNAME);
 $server->run();
 die;