]> git.defcon.no Git - hermes/commitdiff
Merge branch 'auth-refactor'
authorJon Langseth <jon.langseth@lilug.no>
Sat, 21 Jan 2012 01:36:26 +0000 (02:36 +0100)
committerJon Langseth <jon.langseth@lilug.no>
Sat, 21 Jan 2012 01:36:26 +0000 (02:36 +0100)
16 files changed:
api/alias.php
api/auth.php [new file with mode: 0644]
api/config.php.sample
api/domain.php
api/lib/auth.php [deleted file]
api/lib/auth_base.php [new file with mode: 0644]
api/lib/auth_plugins/permitall.php [new file with mode: 0644]
api/numbers.php
api/phone.php
api/t/auth.t [new file with mode: 0644]
api/t/domain.t
api/t/tests_common.pm [new file with mode: 0644]
api/t/user.t
api/user.php
doc/api-nodes.txt
doc/sql-data/structure.sql

index 6451b2eef3efa52c774c1997afbd9b072df6f3e5..5bbdbf2c4a9e93b696b1f77c20284d5efb2d73fc 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 require_once('config.php');
+require_once('lib/auth_base.php');
 require_once('lib/user_functions.php');
 require_once('lib/number_functions.php');
 require_once('lib/common_functions.php');
@@ -19,6 +20,8 @@ if ( !$config['sql_link'] )
        exit;
 }
 
+token_auth();
+
 //*************************************************************************************        
        switch ( $_SERVER['PATH_INFO'] )
        {
diff --git a/api/auth.php b/api/auth.php
new file mode 100644 (file)
index 0000000..50050e5
--- /dev/null
@@ -0,0 +1,278 @@
+<?php
+require_once('config.php');
+require_once('lib/auth_base.php');
+require_once('lib/common_functions.php');
+require_once('lib/db_functions.php');
+require_once('lib/domain_functions.php');
+
+$config = get_config();
+
+$config['sql_link'] = @mysql_connect( 
+       $config['sql_server'],
+       $config['sql_username'],
+       $config['sql_password']
+);
+if ( !$config['sql_link'] )
+{
+       print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
+       exit;
+}
+
+//*************************************************************************************        
+if ( ( $_SERVER['PATH_INFO'] == "/login" ) || ( $_SERVER['PATH_INFO'] == "/logout" ) )
+{
+       switch ( $_SERVER['PATH_INFO'] )
+       {
+               case "/login":
+                       // Allow login using username and password, or API key.
+                       // On successful login, a named session should be started,
+                       // some data related to the session should be stored,
+                       // and the name of the session provided to the user
+                       // in the result.
+                       $type = false;
+                       $authid = false;
+
+                       if ( array_key_exists('username', $_GET) 
+                               && array_key_exists('password', $_GET) )
+                       {
+                               if ( 1 == authuser_verify( sql_clean($_GET['username']), sql_clean($_GET['password'])))
+                               {
+                                       $type = "user";
+                                       $authid = $_GET['username'];
+                               }
+                               else
+                               {
+                                       print json_encode( array( 'response' => 'failed', 'cause' => 'unauthorized', 'description' => 'Login failed') );
+                                       exit;
+                               }
+                       }
+                       else if ( array_key_exists('api_key', $_GET) )
+                       {
+                               if ( verify_apikey( sql_clean( $_GET['api_key'] ) ) == 1 )
+                               {
+                                       $type = "key";
+                                       $authid = $_GET['api_key'];
+                               }
+                               else
+                               {
+                                       print json_encode( array( 'response' => 'failed', 'cause' => 'unauthorized', 'description' => 'Login failed') );
+                                       exit;
+                               }
+                       }
+                       else
+                       {
+                               print json_encode ( array( 'response' => 'invalid') );
+                               break;  
+                       }
+                       $session_name = set_credentials( $authid, $type );
+                       $auth_key = update_authkey( $session_name, $authid );
+                       print json_encode( array( 'response' => 'ok', 'session' => $session_name, 'auth_key' => $auth_key ));
+                       break;
+               case "/logout":
+                       // De-authenticate/deauthorize the ongoing session.
+                       // I.e. destroy session data, remove session cookies.
+                       $session_name = "";
+                       if ( array_key_exists('session', $_GET ) )
+                               $session_name = $_GET['session'];
+                       session_name($session_name);
+                       session_start();
+                       clear_credentials($session_name);
+
+                       if ( $_SESSION )
+                               print json_encode ( array( 'response' => 'wtffailed?') );
+                       else
+                               print json_encode ( array( 'response' => 'ok') );
+                       break;
+               default:
+                       print json_encode ( array( 'response' => 'invalid') );
+       }
+}
+else
+{
+       token_auth();
+
+       switch ( $_SERVER['PATH_INFO'] )
+       {
+               case "/ping":
+                       // API clients are required to periodically ping the server
+                       // The time between pings (interval) is 5 minutes?
+                       // A ping call refreshes cookie lifetimes, then
+                       // generates and stores a new auth_key
+                       // The ping required a valid session...
+                       // A successful ping returns a 'response' => 'pong'
+                       // along with the new auth_key.
+                       $session_name = $_GET['session'];
+                       $authid = $_SESSION['authid'];
+                       $auth_key = update_authkey( $session_name, $authid );
+                       print json_encode( array( 'response' => 'pong', 'auth_key' => $auth_key ));
+                       break;
+               case "/new_apikey":
+                       // If the current authorization has write access, create
+                       // a new API key with requested access (ro/rw).
+                       if ( ! can_write() )
+                               simple_authfail();
+
+                       if ( array_key_exists('host_ip', $_GET )
+                               && array_key_exists('access', $_GET ))
+                       {
+                               $host = $_GET['host_ip'];
+                               $access = $_GET['access'];
+
+                               if (! preg_match("/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/", $host) || ! authlevel_value( $access ) )
+                               {
+                                       print json_encode ( array( 'response' => 'invalid', 'cause' => 'parameters' ) );
+                                       break;  
+                               }
+                               $level = authlevel_value( $access );
+                               $key = add_apikey( $host, $level );
+                               if ( ! $key )
+                               {
+                                       print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.'));
+                                       break;
+                               }
+                               print json_encode( array( 'response' => 'ok', 'key' => $key, 'host' => $host, 'access' => authlevel_name( get_authorization( "key", $key ) ) ) );
+                               break;
+                       }
+                       else print json_encode ( array( 'response' => 'invalid') );
+                       break;  
+               case "/remove_apikey":
+                       // If the current authorization has write access,
+                       // remove the given API key.
+                       if ( ! can_write() )
+                               simple_authfail();
+
+                       if ( array_key_exists('api_key', $_GET ) )
+                       {
+                               $key = sql_clean( $_GET['api_key'] );
+                               // Perform a key-verification, skipping host/remote-address check.
+                               if ( ! verify_apikey( $key, true ) )
+                               {
+                                       print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant'));
+                                       break;
+                               }
+                               if ( ! remove_apikey( $key ) )
+                               {
+                                       print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.'));
+                                       break;
+                               }
+                               print json_encode( array( 'response' => 'ok', 'key' => $key ) );
+                               break;
+                       }
+                       else print json_encode ( array( 'response' => 'invalid') );
+                       break;  
+               case "/list_apikeys":
+                       // List valid API keys.
+                       // Fail is current authorization does not have write access.
+                       if ( ! can_write() )
+                               simple_authfail();
+                       $list = list_apikeys();
+                       print json_encode( array( 'response' => 'ok', 'list' => $list ) );
+                       break;
+               case "/authorize_user":
+                       // Add or update a valid back-end user in authorization
+                       // if the current authentication has write access.
+                       // Since the user exists in backend, the only
+                       // needed parameters should be username and access level
+                       // If the authorization does not exist, add it.
+                       // If the user is already authorized, replace access level.
+                       if ( ! can_write() )
+                               simple_authfail();
+
+                       if ( array_key_exists('username', $_GET )
+                               && array_key_exists('access', $_GET ))
+                       {
+                               $user = $_GET['username'];
+                               $access = $_GET['access'];
+                               $level = authlevel_value( $access );
+
+                               if ( ! $level )
+                               {
+                                       print json_encode ( array( 'response' => 'invalid', 'cause' => 'parameters' ) );
+                                       break;  
+                               }
+                               if ( ! authuser_getinfo( $user ) )
+                               {
+                                       print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant'));
+                                       break;
+                               }
+
+                               if ( ! update_authorization( "user", $user, $level ) )
+                               {
+                                       print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.'));
+                                       break;
+                               }
+
+                               print json_encode( array( 'response' => 'ok', 'user' => $user, 'access' => authlevel_name( get_authorization( "user", $user ) ) ) );
+                               break;
+                       }
+                       else print json_encode ( array( 'response' => 'invalid') );
+                       break;  
+
+               case "/remove_user":
+                       // If the current authentication has write access:
+                       // Remove authorization for the given users.
+                       // Delete user from backend if backend is read-write.
+                       if ( ! can_write() )
+                               simple_authfail();
+
+                       if ( array_key_exists('username', $_GET ))
+                       {
+                               $user = $_GET['username'];
+
+                               $t_level = get_authorization( "user", $user ); 
+
+                               if ( $t_level && ! remove_authorization( $user ) )
+                               {
+                                       print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.'));
+                                       break;
+                               }
+                               if ( ! authmethod_readonly() )
+                               {
+                                       if ( !authuser_getinfo( $user ) )
+                                       {
+                                               print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant'));
+                                               break;
+                                       }
+                                       if ( !authuser_delete( $user ) )
+                                       {
+                                               print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.'));
+                                               break;
+                                       }
+                               }
+
+                               print json_encode( array( 'response' => 'ok', 'user' => $user ) );
+                               break;
+                       }
+                       else print json_encode ( array( 'response' => 'invalid') );
+                       break;  
+
+               case "/list_users":
+                       // List valid API user-acounts.
+                       // Fail with notauthorized if current authentication
+                       // does not have write access.
+                       // Should not return users from backend, 
+                       // but should only return users with authorization.
+                       if ( ! can_write() )
+                               simple_authfail();
+                       $list = list_authusers();
+                       print json_encode( array( 'response' => 'ok', 'list' => $list ) );
+                       break;
+
+               case "/add_user":
+                       // Add user to backend if backend is read-write and
+                       // the current authentication has write access.
+                       // The created user should be added to authorizations
+                       // with an access level of "limited_read (1)"
+               case "/update_user":
+                       // Update the given user in the backend, if the backend
+                       // is read-write, and the current authentication has
+                       // write access.
+                       print json_encode ( array( 'response' => 'notimplemented') );
+                       break;
+               default:
+                       print json_encode ( array( 'response' => 'invalid') );
+       }
+}
+//*************************************************************************************        
+mysql_close( $config['sql_link'] );
+?>
index 66d03dc3a2b02a114ef23d954b4d6f8625a112a9..58a26b4108fc26ad14316322671165da85928e65 100644 (file)
@@ -1,9 +1,9 @@
 <?php
 
-require_once('lib/auth.php');
 function get_config()
 {
        return array(
+               'auth_backend' => 'permitall',
                'sql_server' => 'localhost',
                'sql_username' => 'dbusername',
                'sql_password' => 'dbuserpass',
@@ -16,6 +16,10 @@ function get_config()
                'provision_users_table' => 'users',
                'provision_phones_table' => 'phones',
                'provision_servers_table' => 'servers',
+               'apikeys_table' => 'apikeys',
+               'authorizations_table' => 'authorizations',
+               'sessionkeys_table' => 'sessionkeys',
+               'sessionkey_lifetime' => 5, // Minutes
                'numbers_table' => 'number_pool',
                'standard_dialplan' => '(*xx*.|xxx.)',
                'default_domain' => 'hig.no',
index b91a347bf602dfa1280593f914da22ac88bd4a31..7c8a797b6067dd84339ad9a08032a7b6d61e3966 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 require_once('config.php');
+require_once('lib/auth_base.php');
 require_once('lib/common_functions.php');
 require_once('lib/db_functions.php');
 require_once('lib/domain_functions.php');
@@ -17,6 +18,8 @@ if ( !$config['sql_link'] )
        exit;
 }
 
+token_auth();
+
 //*************************************************************************************        
        switch ( $_SERVER['PATH_INFO'] )
        {
diff --git a/api/lib/auth.php b/api/lib/auth.php
deleted file mode 100644 (file)
index 2f8f9cf..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-function token_auth( )
-{
-       return true;
-}
-
-if (! token_auth() )
-{
-       print json_encode( array( 'response' => 'failed', 'cause' => 'unauthorized', 'description' => 'Not authorized') );
-       exit;
-}
-?>
diff --git a/api/lib/auth_base.php b/api/lib/auth_base.php
new file mode 100644 (file)
index 0000000..bb3c26f
--- /dev/null
@@ -0,0 +1,418 @@
+<?php
+require_once('config.php');
+
+$config = get_config();
+
+function authlevel_value( $level )
+{
+       switch ( $level )
+       {
+               case 'limited_read':
+                       return 1;
+               case 'full_read':
+                       return 2;
+               case 'read_write':
+                       return 3;
+               default:
+                       return 0;
+       }
+}
+function authlevel_name( $level )
+{
+       switch ( $level )
+       {
+               case 1:
+                       return 'limited_read';
+               case 2:
+                       return 'full_read';
+               case 3:
+                       return 'read_write';
+               default:
+                       return 'no_access';
+       }
+}
+
+/*******************************
+* Load authentication plugin ..
+*******************************/
+if ( preg_match('/^\w+$/', $config['auth_backend']))
+{
+       if ( !@include_once ( 'lib/auth_plugins/' . $config['auth_backend'] . ".php" ) )
+       {  print json_encode( array( 'response' => 'error', 'cause' => 'auth-load' ) ); exit; }
+}
+else
+{  print json_encode( array( 'response' => 'error', 'cause' => 'config-error' ) ); exit; }
+/*******************************/
+
+function new_key( $hex = false )
+{
+       // Basically this is at the moment a slightly modified
+       // version of generate_password() from user_functiions.php
+       // The behaviour/output of this function is expected to change
+       // so using generate_password() directly does not make sense...
+       $length = 16;
+       $string = "";
+        while ( strlen( $string ) < $length )
+        {
+               if ( $hex )
+                       $string .= substr(md5(rand().rand()), 0, $length+1);
+               else
+               {
+                       $string .= crypt( substr(sha1(rand()), 0, $length+1) );
+                       $string = preg_replace( '/\W/', '', $string);
+               }
+        }
+       return substr( $string, 1, $length );
+}
+
+function simple_authfail()
+{
+       print json_encode( array( 'response' => 'failed', 'cause' => 'unauthorized', 'description' => 'Not authorized') );
+       exit;
+}
+
+function token_auth( )
+{
+       global $_GET;
+
+       // TODO: Part of ping/pong requirement.
+       // Run a function to clear all authkeys older than 5 minutes.
+       expire_authkeys();
+
+       if ( array_key_exists('session', $_GET ) 
+            && array_key_exists('auth_key', $_GET ) )
+       { 
+               if ( ! check_session($_GET['session'] ) ) simple_authfail();
+               if ( ! check_authkey($_GET['auth_key'] ) ) simple_authfail(); 
+       }
+       else simple_authfail();
+}
+
+function get_cookie_path ()
+{
+       $name = $_SERVER["SCRIPT_NAME"];
+       $file = basename($name);
+       $path = preg_replace("/".$file."/", "", $name);
+       return $path;
+
+}
+
+function check_authkey ( $key )
+{
+       // TODO: Make real, actual checks...
+       if ( $key ) return true;
+       return false;
+}
+
+function expire_authkeys()
+{
+       global $config;
+
+       // Force deletion of sessions that have expired keys.
+       $query = sprintf("SELECT session, sessid FROM %s WHERE `last` < DATE_SUB( NOW(), INTERVAL %d MINUTE)",
+               $config['sessionkeys_table'],
+               $config['sessionkey_lifetime']);
+       $result = sql_dbquery( $config['provision_db'], $query );
+       while ( $row = @mysql_fetch_row( $result ) )
+       {
+               remove_session( $row[0], $row[1] );
+       }
+
+       $query = sprintf("DELETE FROM %s WHERE `last` < DATE_SUB( NOW(), INTERVAL %d MINUTE)",
+               $config['sessionkeys_table'],
+               $config['sessionkey_lifetime']);
+
+       sql_dbexec( $config['provision_db'], $query );
+}
+
+function update_authkey ( $session, $authid )
+{
+       global $config;
+
+       $key = substr(new_key(), 0, 8);
+
+       expire_authkeys();
+
+       // TODO: Refresh cookie
+
+       $remote = $_SERVER['REMOTE_ADDR'];
+       $query = sprintf("INSERT INTO %s ( `sessid`, `session`, `authid`, `client`, `key`, `last` )
+                               VALUES ( '%s', '%s', '%s', '%s', '%s', NOW() )
+                               ON DUPLICATE KEY UPDATE `key` = '%s', `last` = NOW()",
+               $config['sessionkeys_table'],
+               session_id(),
+               session_name(),
+               sql_clean($authid),
+               sql_clean($remote),
+               sql_clean($key),
+               sql_clean($key));
+       if ( ! sql_dbexec( $config['provision_db'], $query ) )
+       {
+               mysql_error();
+       }
+       $_SESSION['kkey'] = $key;
+       $_SESSION['when'] = time();
+       return $key;
+}
+
+function check_session ( $name )
+{
+       session_name( $name );
+       session_start();
+       if ( ! $_SESSION['authid'] )
+       {
+               return clear_credentials($name);
+       }
+       if ( ! $_COOKIE['client_key'] )
+       {
+               return clear_credentials($name);
+       }
+
+       $authid = $_SESSION['authid'];
+       $type   = $_SESSION['type'];
+       $client_key = $_COOKIE['client_key'];
+
+       $level = get_authorization( $type, $authid );
+       if ( $level == false )
+       {
+               return clear_credentials($name);
+       }
+
+       $session_key = md5( $name . $authid );
+       if ( $client_key != $session_key )
+       {
+               return clear_credentials($name);
+       }
+
+       // If we got this far, things are looking good.
+       return true;
+}
+
+function set_credentials( $authid, $type )
+{
+       $name = new_key(true);
+       session_name( $name );
+       session_start();
+       $_SESSION['authid'] = $authid;
+       $_SESSION['type']   = $type;
+
+       $client_key  = md5( $name . $authid );
+       setcookie('client_key', $client_key, time()+180*60, get_cookie_path() );
+
+       return $name;
+}
+
+function clear_credentials($name)
+{
+       global $config;
+
+       setcookie('client_key', '', 0, get_cookie_path() );
+
+       remove_session($name);
+       $_SESSION = array();
+
+       $query = sprintf("DELETE FROM %s WHERE `session` = '%s'",
+               $config['sessionkeys_table'],
+               sql_clean($name));
+       sql_dbexec( $config['provision_db'], $query );
+
+       return false;
+}
+
+function remove_session ($name, $id = null )
+{
+       if ( $id == null )
+       {
+               session_destroy();
+               setcookie($name, '', 0, "/");
+               return;
+       }
+       $current_session = session_name( );
+       $current_sessid = session_id( );
+       session_commit();
+
+       session_id( $id );
+       session_start();
+       setcookie( $name, '', 0, "/");
+       $_SESSION=array();
+       session_destroy();
+
+       if ( $current_session && $current_session != $name )
+       {
+               session_id($current_sessid);
+               session_start();
+       }
+}
+function add_apikey ( $host, $level )
+{
+       global $config;
+       if ( !is_numeric($level) ) return false;
+
+       $key = new_key();
+
+       // Try to add the new key to authorizations first. If this
+       // fails, there will be the least amount of data to clean up ...
+       if ( ! update_authorization( "key", $key, $level ) ) return false;
+
+       $query = sprintf("INSERT INTO %s ( host, apikey ) VALUES ( '%s', '%s' )",
+               $config['apikeys_table'],
+               sql_clean($host),
+               sql_clean($key));
+
+       if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false;
+       return $key;
+}
+
+function remove_apikey( $key )
+{
+       global $config;
+       if ( ! verify_apikey( $key, true ) ) return false;
+       if ( ! remove_authorization( $key ) ) return false;
+
+       $query = sprintf("DELETE FROM %s WHERE apikey = '%s'",
+               $config['apikeys_table'],
+               sql_clean($key) );
+       if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false;
+
+       return true;
+}
+
+function verify_apikey( $key, $skip_hostcheck = false )
+{
+       global $config;
+
+       $query = sprintf("SELECT host FROM %s WHERE apikey = '%s'",
+               $config['apikeys_table'],
+               sql_clean($key) );
+       $row = sql_dbquery_single( $config['provision_db'], $query );
+       if  (!$row) return false;
+       $host = $row['host'];
+
+       if ( $host && ( $skip_hostcheck ) )
+               return true;
+
+       if ( $host == $_SERVER['REMOTE_ADDR'] ) return true;
+       return false;
+
+}
+
+function list_apikeys ()
+{
+       global $config;
+       $query = sprintf("SELECT k.apikey AS apikey, k.host AS host, 
+                               a.access_level AS access_level 
+                               FROM %s k INNER JOIN %s a ON k.apikey = a.authid
+                               WHERE a.type = 'key'",
+                       $config['apikeys_table'], 
+                       $config['authorizations_table']);
+       $list = array();
+       $result = sql_dbquery( $config['provision_db'], $query);
+       if ( ! $result ) return $list; 
+       while ( $row = @mysql_fetch_assoc( $result ) )
+       {
+               array_push( $list, array(
+                       'api_key' => $row['apikey'],
+                       'host' => $row['host'],
+                       'level' => authlevel_name( $row['access_level'] )
+               ));
+       }
+       return $list;
+
+}
+
+function list_authusers ()
+{
+       global $config;
+       $query = sprintf("SELECT authid, access_level
+                               FROM %s
+                               WHERE type = 'user'",
+                       $config['authorizations_table']);
+       $list = array();
+       $result = sql_dbquery( $config['provision_db'], $query);
+       if ( ! $result ) return $list; 
+       while ( $row = @mysql_fetch_assoc( $result ) )
+       {
+               $username = $row['authid'];
+               $user_data = authuser_getinfo( $username );
+
+               // TODO: Remove invalid users here?
+               if ( ! $user_data ) continue;
+
+               array_push( $list, array(
+                       'user' => $username,
+                       'name' => $user_data['name'],
+                       'email' => $user_data['email'],
+                       'level' => authlevel_name( $row['access_level'] )
+               ));
+       }
+       return $list;
+
+}
+
+
+
+function update_authorization( $type, $authid, $level )
+{
+       global $config;
+       if ( !is_numeric($level) ) return false;
+       if ( ($type != "key") && ($type != "user") ) return false;
+
+       $query = sprintf("INSERT INTO %s ( authid, type, access_level ) VALUES ( '%s', '%s', %d )
+                       ON DUPLICATE KEY UPDATE access_level=%d",
+               $config['authorizations_table'],
+               sql_clean($authid),
+               $type,
+               $level, $level);
+       if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false;
+       return true;
+}
+
+function remove_authorization( $authid )
+{
+       global $config;
+       $query = sprintf("DELETE FROM %s WHERE authid = '%s'",
+               $config['authorizations_table'],
+               sql_clean($authid) );
+       //print $query . "\n\n";
+       if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false;
+       return true;
+}
+
+
+function get_authorization( $type, $authid )
+{
+       global $config;
+
+       // If API-key is used, but key fails verification, write is impossible.
+       if ( ( $type == "key" ) && ( ! verify_apikey( $authid, true ) ) )
+               return false;
+
+       // If User-login is used, but backend is unable to provide info, fail.
+       if ( ( $type == "user" ) && ( ! authuser_getinfo( $authid ) ) )
+               return false;
+
+       // The only types of access control supported are "user" or "key".
+       if ( ($type != "user" ) && ($type != "key") )
+               return false;
+
+       $query = sprintf("SELECT access_level FROM %s  WHERE authid = '%s'",
+               $config['authorizations_table'],
+               sql_clean($authid) );
+       $row = sql_dbquery_single( $config['provision_db'], $query );
+       if  (!$row) return false;
+       $level = $row['access_level'];
+       return $level;
+}
+
+function can_write ( )
+{
+       // Stub, to be called on any API nodes that write data in the DB.
+       $authid = $_SESSION['authid'];
+       $type   = $_SESSION['type'];
+
+       $level = get_authorization( $type, $authid );
+       if ( $level >= authlevel_value('read_write') ) return $level;
+       else return false;
+}
+
+?>
diff --git a/api/lib/auth_plugins/permitall.php b/api/lib/auth_plugins/permitall.php
new file mode 100644 (file)
index 0000000..8bcd2ba
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+/*
+       permitall.php is a sample authentication plugin
+       that responds with 'accept' regardless of what
+       usernames and passwords are passed to it.
+
+       This plugin should serve as a sample plugin:
+       all authentication plugins must implement
+       all functions defined here.
+
+       NOTE that the auth-plugins are for authentication
+       only, and is not doing any kind of authorization.
+
+       NOTE that the auth-plugins handle user
+       authentication for API functions....
+
+*/
+
+// Is the backend readonly?
+function authmethod_readonly ()
+{
+       // Each auth-plugin must specify if users can
+       // be modified in the backend by responding to
+       // the authmethod_readonly with a true/false.
+       //
+       // By returning false to a readonly-poll, the
+       // plugin should be able to add users to the
+       // backend, and also must be able to change
+       // user data and passwords.
+       return true;
+}
+
+// Fetch user geckos (basic display info)
+function authuser_getinfo ( $username )
+{
+       // Obviously we are returning dummy data here.
+       // on a real auth method, valid returns values
+       // would be a $user array on success, or false
+       // on error.
+       $user['name']     = "Default User";
+       $user['email']    = "example@example.com";
+       return $user;
+}
+
+// Update geckos-info for user in backend
+function authuser_setinfo ( $username, $name, $email )
+{
+       // RW plugins should return false on failure,
+       // and true on success updating user information
+       // RO plugins should always return false
+       return false;
+}
+
+// Change a user-password in the backend
+function authuser_password ( $username, $password )
+{
+       // RW plugins should return false on failure,
+       // and true on success updating user information
+       // RO plugins should always return false
+       return false;
+}
+
+// Add a user to the backend
+function authuser_add ( $username, $name, $email, $password )
+{
+       // RW plugins should return false on failure,
+       // and true on success updating user information
+       // RO plugins should always return false
+       return false;
+}
+
+// Remove a user from the backend.
+function authuser_delete ( $username )
+{
+       // RW plugins should return false on failure,
+       // and true on success updating user information
+       // RO plugins should always return false
+       return false;
+}
+
+// Username+password verification. Basically "login"
+function authuser_verify ( $username, $password )
+{
+       // This plugin will always accept.
+       // A real plugin should naturally perform strong user
+       // verification. 
+       //
+       // Valid return values from this function:
+       //  * -1 -> Failure (e.g. backend not available)
+       //  * 0  -> username/password rejected
+       //  * 1  -> username/password accepted
+
+       return 1;
+}
+
+?>
index e9fc796e3881881625425699fadbf1205e609477..7874394a1a98a65a685cffc0552ddcdf93e27658 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 require_once('config.php');
-//require_once('lib/user_functions.php');
+require_once('lib/auth_base.php');
 require_once('lib/common_functions.php');
 require_once('lib/db_functions.php');
 require_once('lib/number_functions.php');
@@ -18,6 +18,8 @@ if ( !$config['sql_link'] )
        exit;
 }
 
+token_auth();
+
 //*************************************************************************************        
        switch ( $_SERVER['PATH_INFO'] )
        {
index 9437d66b47738961781d344ca57c5fd2172d5573..a47c4946ff9883924a16cf39bfd621adbf934c05 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 require_once('config.php');
+require_once('lib/auth_base.php');
 require_once('lib/user_functions.php');
 require_once('lib/common_functions.php');
 require_once('lib/db_functions.php');
@@ -17,6 +18,7 @@ if ( !$config['sql_link'] )
        print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
        exit;
 }
+token_auth();
 
 //*************************************************************************************        
        switch ( $_SERVER['PATH_INFO'] )
diff --git a/api/t/auth.t b/api/t/auth.t
new file mode 100644 (file)
index 0000000..d01c52b
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/perl
+
+use strict;
+use Test::More 'no_plan';
+use tests_common;
+
+# Configuration variables defined in tests_common
+# Make sure those are updated/set correctly..
+#   $api_base
+#   $api_key
+#
+# Further variables defined in tests_common:
+#   $invalid_key
+#   $test_username
+#   $test_password
+#   $invalid_username
+
+my ($response, $data, $temp);
+
+isa_ok( $g_ua, 'LWP::UserAgent', '$g_ua');
+isa_ok( $g_ua->cookie_jar, 'HTTP::Cookies', '$g_ua->cookies');
+
+login_apikey();
+
+$data = exec_apinode('auth/new_apikey', { 'host_ip' => '10.0.3.87', 'access' => 'full_read' });
+is( $data->{'response'}, 'ok',                 'auth/new_apikey ok');
+ok( $data->{'key'},                    'auth/new_apikey new key');
+$temp = $data->{'key'} if ( $data->{'response'} eq 'ok' );
+undef $data;
+
+$data = exec_apinode('auth/list_apikeys', undef);
+is( $data->{'response'}, 'ok',                 'auth/list_apikeys ok');
+ok( $data->{'list'},                   'auth/list_apikeys list');
+undef $data;
+
+$data = exec_apinode('auth/remove_apikey', { "api_key" => $temp });
+is( $data->{'response'}, 'ok',                 'auth/remove_apikey ok');
+ok( $data->{'key'},                    'auth/remove_apikey key');
+undef $data;
+
+$data = exec_apinode("auth/authorize_user", { "username" => $test_username, "access" => "read_write" });
+is( $data->{'response'}, 'ok',                 'auth/authorize_user ok');
+ok( $data->{'user'},                   'auth/authorize_user user set');
+is( $data->{'user'}, $test_username,   'auth/authorize_user user is ' . $test_username);
+ok( $data->{'access'},                         'auth/authorize_user access set');
+isnt( $data->{'access'}, 'no_access',  'auth/authorize_user level');
+undef $data;
+
+$data = exec_apinode("auth/list_users", undef );
+is( $data->{'response'}, 'ok',                 'auth/list_users ok');
+ok( $data->{'list'},                   'auth/list_users list');
+undef $data;
+
+$data = exec_apinode("auth/ping", undef );
+is( $data->{'response'}, 'pong',               'auth/ping PONG!');
+ok( $data->{'auth_key'},                               'auth/ping key set');
+ok( not ($data->{'auth_key'} eq $auth_key),            'auth/ping key changed');
+$auth_key = $data->{'auth_key'};
+undef $data;
+
+$data = exec_apinode("auth/list_apikeys", undef );
+is( $data->{'response'}, 'ok',                 'auth/ping new key accepted.');
+undef $data;
+
+
+logout();
+ok( !$session,                         'logged out, session cleared');
+login_user();
+
+$data = exec_apinode("auth/authorize_user", { "username" => $test_username, "access" => "full_read" });
+is( $data->{'response'}, 'ok',                 'auth/authorize_user as user ok');
+is( $data->{'user'}, $test_username,   'auth/authorize_user user is ' . $test_username);
+ok( $data->{'access'},                         'auth/authorize_user access set');
+isnt( $data->{'access'}, 'no_access',  'auth/authorize_user level');
+undef $data;
+
+$data = exec_apinode("auth/remove_user", { "username" => $test_username });
+is( $data->{'response'}, 'failed',             'auth/remove_user as nonpriv user fails');
+is( $data->{'cause'}, 'unauthorized',                  'auth/remove_user cause correct');
+undef $data;
+
+
+$data = exec_apinode("auth/add_user", { "username" => $test_username });
+is( $data->{'response'}, 'notimplemented',             'auth/add_user TODO: notimplemented.');
+undef $data;
+
+$data = exec_apinode("auth/update_user", { "username" => $test_username });
+is( $data->{'response'}, 'notimplemented',             'auth/update_user TODO: notimplemented.');
+undef $data;
+
+logout();
+ok( !$session,                         'logged out, session cleared');
+login_apikey();
+
+$data = exec_apinode("auth/remove_user", { "&username" => $test_username });
+is( $data->{'response'}, 'ok',                 'auth/remove_user as user ok');
+is( $data->{'user'}, $test_username,   'auth/remove_user removed ' . $test_username);
+undef $data;
+
+logout();
+ok( !$session,                         'logged out, session cleared');
+
+$data = exec_apinode("auth/list_apikeys", undef );
+is( $data->{'response'}, 'failed',             'auth/list_apikeys denied after logout');
+is( $data->{'cause'}, 'unauthorized',          'auth/list_apikeys cause correct');
+undef $data;
+
+$data = exec_apinode("auth/list_users", undef );
+is( $data->{'response'}, 'failed',             'auth/list_users denied after logout');
+is( $data->{'cause'}, 'unauthorized',          'auth/list_users cause correct');
+undef $data;
+
index 917ab1657273a74327a53e14d2e195aaafec01af..69882035b81f896bdb7d612ad8bc740cf212025d 100644 (file)
@@ -1,57 +1,37 @@
 #!/usr/bin/perl
 
-use Test::More 'no_plan';
-
 use strict;
-use LWP;
-use Data::Dumper;
-use JSON;
-
-my $api_base  = "http://10.0.2.5/hermes/api/";
-my $api_key   = "6327c08b70f9";
-my $api_user  = "test";
-my $api_pass  = "Very5ecr3t";
-
-my $test_domain = undef;
+use Test::More 'no_plan';
+use tests_common;
 
-# plan tests => 2;
-my ($g_ua, $response, $data, $temp, @t);
+# Configuration variables defined in tests_common
+# Make sure those are updated/set correctly..
+#   $api_base
+#   $api_key
+#
+# Further variables defined in tests_common:
+#   $invalid_key
+#   $test_username
+#   $test_password
+#   $invalid_username
 
+my ($data, $temp, $test_domain, $test_remote_domain );
 
-$g_ua = LWP::UserAgent->new;
 isa_ok( $g_ua, 'LWP::UserAgent', '$g_ua');
-
-$g_ua->cookie_jar({}); # In-memory jar, look at HTTP::Cookies for persistant
 isa_ok( $g_ua->cookie_jar, 'HTTP::Cookies', '$g_ua->cookies');
 
-TODO: {
-       local $TODO = 'auth/login not implemented yet';
-       $response = $g_ua->get( $api_base . "auth/login?username=" . $api_user .
-                "password=" . $api_pass . "&key=" . $api_key);
-
-       #$data = decode_json( $response->content);
-       is( $data->{'response'}, 'ok',          'auth/login');
-       undef $response; undef $data;
-}
+login_apikey();
 
-$response = $g_ua->get( $api_base . "domain/list");
-isa_ok( $response, 'HTTP::Response',   'domain/list $response');
-ok ($response->is_success,             'domain/list is_success');
-$data = decode_json( $response->content);
-ok($data,                              'domain/list JSON decode');
+$data = exec_apinode("domain/list", undef);
 is( $data->{'response'}, 'ok',                 'domain/list result');
 ok($data->{'list'},                    'domain/list array');
 
 # NOW: Set the $test_domain to something useful (i.e. the first reported domain)
 $test_domain = $data->{'list'}[0];
 ok($test_domain,                       'test_domain set.');
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "domain/get_servers?domain=" . $test_domain );
-isa_ok( $response, 'HTTP::Response',   'domain/list $response');
-ok ($response->is_success,             'domain/list is_success');
-$data = decode_json( $response->content);
-ok($data,                              'domain/get_servers JSON decode');
+$data = exec_apinode("domain/get_servers", { "domain" => $test_domain });
 is( $data->{'response'}, 'ok',                 'domain/get_servers result');
 ok($data->{'servers'}->{'domain'},     'domain/get_servers - domain');
 ok($data->{'servers'}->{'registrar'},  'domain/get_servers - registrar');
@@ -59,20 +39,17 @@ ok($data->{'servers'}->{'r_port'},  'domain/get_servers - r_port');
 ok($data->{'servers'}->{'proxy'},      'domain/get_servers - proxy');
 ok($data->{'servers'}->{'p_port'},     'domain/get_servers - p_port');
 ok($data->{'servers'}->{'prov_url'},   'domain/get_servers - prov_url');
-undef $response; undef $data;
-
-$response = $g_ua->get( $api_base . "domain/set_servers" .
-       "?domain=" . $test_domain .
-       "&registrar=registrar." . $test_domain .
-       "&r_port=5060" .
-       "&proxy=proxy." . $test_domain .
-       "&p_port=5060" .
-       "&prov_url=http://phone." . $test_domain . "/hermes/prov/" );
+undef $data;
+
+$data = exec_apinode("domain/set_servers", {
+       "domain" =>  $test_domain,
+       "registrar" => "registrar." . $test_domain,
+       "r_port" => 5060,
+       "proxy" => "proxy." . $test_domain,
+       "p_port" => 5060,
+       "prov_url" => "http://phone." . $test_domain . "/hermes/prov/",
+} );
        
-isa_ok( $response, 'HTTP::Response',   'domain/list $response');
-ok ($response->is_success,             'domain/list is_success');
-$data = decode_json( $response->content);
-ok($data,                              'domain/set_servers JSON decode');
 is( $data->{'response'}, 'ok',                 'domain/set_servers result');
 ok($data->{'servers'}->{'domain'},     'domain/set_servers - domain');
 ok($data->{'servers'}->{'registrar'},  'domain/set_servers - registrar');
@@ -80,12 +57,7 @@ ok($data->{'servers'}->{'r_port'},   'domain/set_servers - r_port');
 ok($data->{'servers'}->{'proxy'},      'domain/set_servers - proxy');
 ok($data->{'servers'}->{'p_port'},     'domain/set_servers - p_port');
 ok($data->{'servers'}->{'prov_url'},   'domain/set_servers - prov_url');
-undef $response; undef $data;
+undef $data;
+
+logout();
 
-TODO: {
-       local $TODO = 'auth/logout not implemented yet';
-       $response = $g_ua->get( $api_base . "auth/logout");
-       #$data = decode_json( $response->content);
-       is( $data->{'response'}, 'ok',          'auth/logout');
-       undef $response; undef $data;
-}
diff --git a/api/t/tests_common.pm b/api/t/tests_common.pm
new file mode 100644 (file)
index 0000000..9c2ec83
--- /dev/null
@@ -0,0 +1,130 @@
+package tests_common;
+
+use Test::More;
+
+use strict;
+use warnings;
+
+use LWP;
+use Data::Dumper;
+use JSON;
+
+BEGIN {
+       use Exporter();
+       our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+       @ISA = qw(Exporter);
+       @EXPORT = qw($api_base $api_key $invalid_key $test_username  $test_password  $invalid_username $g_ua $session $auth_key &exec_apinode &login_apikey &login_user &logout);
+       %EXPORT_TAGS = ( 'all' => [ qw( ) ] );
+       @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); 
+}
+our $api_base  = "http://10.0.2.5/hermes/api/";
+our $api_key   = "4hErgw3QFJLeuXGW";
+our $invalid_key = "invalidkey" . int(rand(255));
+
+our $test_username = "testauth-" . int(rand(255));
+our $test_password = "foobarbaz";
+our $invalid_username = "failauth-" . int(rand(255));
+
+our ($g_ua, $session, $auth_key);
+
+$g_ua = LWP::UserAgent->new;
+$g_ua->cookie_jar({}); # In-memory jar, look at HTTP::Cookies for persistant
+
+
+sub exec_apinode($$)
+{
+       my $node = shift;
+       my $param = shift;
+
+       my ( $response, $data );
+
+       $session = "" if not defined $session;
+       $auth_key = "" if not defined $auth_key;
+       my $url = $api_base . $node . 
+               "?session=" . $session .
+               "&auth_key=" . $auth_key;
+
+       foreach my $key ( keys %$param )
+       {
+               $url .= "&" . $key . "=" . $param->{$key};
+       }
+       $response = $g_ua->get( $url );
+       if ( $response->is_success )
+       {
+               if ( $response->content =~ m/\s*{/ )
+               {
+                       $data = decode_json( $response->content);
+               }
+               else
+               {
+                       $data = $response->content;
+               }
+
+       }
+       return $data;
+}
+
+sub login_apikey
+{
+       my $response = $g_ua->get( $api_base . "auth/login" .
+                       "?api_key=" . $api_key );
+       ok ($response->is_success,              'login_apikey request is_success');
+       my $data = decode_json( $response->content) if $response->is_success;
+       die("HTTP error") unless $response->is_success;
+
+       is( $data->{'response'}, 'ok',          'login_apikey logged in');
+       ok( $data->{'session'},                 'login_apikey session set');
+       ok( $data->{'auth_key'},                'login_apikey auth_key set');
+       if ( $data->{'response'} eq "ok" )
+       {
+               $session = $data->{'session'};
+               $auth_key = $data->{'auth_key'};
+       }
+       else
+       {
+               exit;
+       }
+       undef $data; undef $response;
+}
+
+sub login_user
+{
+       my $response = $g_ua->get( $api_base . "auth/login" .
+               "?username=" . $test_username .
+               "&password=" . $test_password );
+
+       ok ($response->is_success,              'login_user request is_success');
+       my $data = decode_json( $response->content) if $response->is_success;
+       die("HTTP error") unless $response->is_success;
+
+       is( $data->{'response'}, 'ok',          'login_user logged in');
+       ok( $data->{'session'},                 'login_user session set');
+       ok( $data->{'auth_key'},                'login_user auth_key set');
+       if ( $data->{'response'} eq "ok" )
+       {
+               $session = $data->{'session'};
+               $auth_key = $data->{'auth_key'};
+       }
+       else
+       {
+               exit;
+       }
+       undef $data; undef $response;
+}
+
+sub logout
+{
+       my $response = $g_ua->get( $api_base . "auth/logout" .
+               "?session=" . $session );
+
+       ok ($response->is_success,              'logout request is_success');
+       die("HTTP error") unless $response->is_success;
+
+       my $data = decode_json( $response->content) if $response->is_success;
+       is( $data->{'response'}, 'ok',          'logout ok');
+
+       undef $session; undef $auth_key;
+}
+
+
+1;
index 81d908dafbe58cc59b256e2ab13b323cad290bd8..fca67eb8acf96c5596cbbbeffe277ee91c41c811 100644 (file)
@@ -1,50 +1,29 @@
 #!/usr/bin/perl
 
-use Test::More 'no_plan';
-
 use strict;
-use LWP;
-use Data::Dumper;
-use JSON;
-
-my $api_base  = "http://10.0.2.5/hermes/api/";
-my $api_key   = "6327c08b70f9";
-my $api_user  = "test";
-my $api_pass  = "Very5ecr3t";
-
-my $test_username = "testuser-" . int(rand(255));
-my $test_aliasname = "testalias-" . int(rand(255));
-
-my $test_domain = undef;
-
-my $test_remote_domain = undef;
-
+use Test::More 'no_plan';
+use tests_common;
 
-# plan tests => 2;
-my ($g_ua, $response, $data, $temp);
+# Configuration variables defined in tests_common
+# Make sure those are updated/set correctly..
+#   $api_base
+#   $api_key
+#
+# Further variables defined in tests_common:
+#   $invalid_key
+#   $test_username
+#   $test_password
+#   $invalid_username
 
+my ($data, $temp, $test_domain, $test_remote_domain );
 
-$g_ua = LWP::UserAgent->new;
 isa_ok( $g_ua, 'LWP::UserAgent', '$g_ua');
-
-$g_ua->cookie_jar({}); # In-memory jar, look at HTTP::Cookies for persistant
 isa_ok( $g_ua->cookie_jar, 'HTTP::Cookies', '$g_ua->cookies');
 
-TODO: {
-       local $TODO = 'auth/login not implemented yet';
-       $response = $g_ua->get( $api_base . "auth/login?username=" . $api_user .
-                "password=" . $api_pass . "&key=" . $api_key);
-
-       #$data = decode_json( $response->content);
-       is( $data->{'response'}, 'ok',          'auth/login');
-       undef $response; undef $data;
-}
+login_apikey();
 
 # First: fetch a supported domain from the API...
-$response = $g_ua->get( $api_base . "domain/list");
-isa_ok( $response, 'HTTP::Response',   'domain/list $response');
-ok ($response->is_success,             'domain/list is_success');
-$data = decode_json( $response->content);
+$data = exec_apinode("domain/list", undef);
 ok($data,                              'domain/list JSON decode');
 is( $data->{'response'}, 'ok',                 'domain/list result');
 ok($data->{'list'},                    'domain/list array');
@@ -55,106 +34,98 @@ $test_remote_domain = "external." . $test_domain;
 
 ok($test_domain,                       'test_domain set.');
 ok($test_remote_domain,                        'test_remote_domain set.');
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/list");
-isa_ok( $response, 'HTTP::Response',   'user/list $response');
-ok ($response->is_success,             'user/list is_success');
-$data = decode_json( $response->content);
+$data = exec_apinode("user/list", undef);
 ok($data,                              'user/list JSON decode');
-is( $data->{'response'}, 'ok',                 'user/list result');
+is($data->{'response'}, 'ok',          'user/list result');
 ok($data->{'list'},                    'user/list array');
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/available?user=" . $test_username . "\@" . $test_domain);
-$data = decode_json( $response->content);
+$data = exec_apinode("user/available", { "user" => $test_username . "\@" . $test_domain });
 is( $data->{'response'}, 'ok',                 'user/available is available');
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/add_local?user=" . $test_username . "\@" . $test_domain . "&displayname=Automatic testing&email=noreply\@" . $test_domain);
-$data = decode_json( $response->content);
+$data = exec_apinode("user/add_local", {
+       "user" => $test_username . "\@" . $test_domain,
+       "displayname" => "Automatic testing",
+       "email" => "noreply\@" . $test_domain,
+} );
 is( $data->{'response'}, 'ok',                 'user/add_local created new account');
 #TODO: Add tests to verify the data from add_local...
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/available?user=" . $test_username . "\@" . $test_domain);
-$data = decode_json( $response->content);
-is( $data->{'response'}, 'failed',             'user/available has been created');
-undef $response; undef $data;
+$data = exec_apinode("user/available", { "user" => $test_username . "\@" . $test_domain });
+is( $data->{'response'}, 'failed',     'user/available has been created');
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/get?user=" . $test_username . "\@" . $test_domain);
-$data = decode_json( $response->content);
+$data = exec_apinode("user/get", { "user" => $test_username . "\@" . $test_domain });
 is( $data->{'response'}, 'ok',                 'user/get returns');
 is( $data->{'user'}->{'username'}, $test_username,     'user/get username correct');
 is( $data->{'user'}->{'domain'},   $test_domain,       'user/get domain correct');
-ok( $data->{'user'}->{'ha1'} =~ m/[a-z0-9]{16,}/,              'user/get ha1 hash generated');
-undef $response; undef $data;
-
-$response = $g_ua->get( $api_base . "user/gen_pw");
-ok( $response->content =~ m/[a-z0-9]{24}/,             'user/gen_pw format OK');
-$temp = $response->content;
-undef $response; undef $data;
-
-$response = $g_ua->get( $api_base . "user/change_pw?user=" . $test_username . "\@" . $test_domain . "&password=" . $temp );
-$data = decode_json( $response->content);
+ok( $data->{'user'}->{'ha1'} =~ m/[a-z0-9]{16,}/,      'user/get ha1 hash generated');
+undef $data;
+
+$data = exec_apinode("user/gen_pw", undef);
+ok( $data =~ m/[a-zA-Z0-9]{24}/,               'user/gen_pw format OK');
+$temp = $data;
+undef $data;
+
+$data = exec_apinode("user/change_pw", {
+       "user" => $test_username . "\@" . $test_domain,
+       "password" => $temp,
+} );
 is( $data->{'response'}, 'ok',                 'user/change_pw');
-undef $response; undef $data;
+undef $data;
 
 # TODO: This tests _almost_ all parameters. Update API docs, then update this test.
-$response = $g_ua->get( $api_base . "user/update?user=" . $test_username . "\@" . $test_domain .
-       "&displayname=Changed" .
-       "&linetext=" .  $test_username . 
-       "&registrar=registrar." . $test_domain . 
-       "&r_port=5061" .
-       "&proxy=proxy." . $test_domain . 
-       "&p_port=5060");
-
-$data = decode_json( $response->content);
+$data = exec_apinode("user/update", {
+       "user" => $test_username . "\@" . $test_domain,
+       "displayname" => "Changed",
+       "linetext" => $test_username,
+       "registrar" =>  "registrar." . $test_domain,
+       "r_port" => 5061,
+       "proxy" => "proxy." . $test_domain,
+       "p_port" => 5060,
+});
 is( $data->{'response'}, 'ok',                 'user/update');
 ok( $data->{'updated'},                        'user/update updated list');
 ok( $data->{'skipped'},                        'user/update skipped list');
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/remove?user=" . $test_username . "\@" . $test_domain);
-$data = decode_json( $response->content);
+$data = exec_apinode("user/remove", { "user" => $test_username . "\@" . $test_domain });
 is( $data->{'response'}, 'ok',                 'user/remove deleted user');
-undef $response; undef $data;
-
-$response = $g_ua->get( $api_base . "user/add_remote?user=" . $test_username . "\@" . $test_domain . 
-       "&displayname=Automatic testing" .
-       "&password=verysecretpasswd" .
-       "&registrar=registrar" . $test_domain);
-
-$data = decode_json( $response->content);
+undef $data;
+
+$data = exec_apinode("user/add_remote", {
+       "user" => $test_username . "\@" . $test_domain,
+       "displayname" => "Automatic testing",
+       "password" => "verysecretpasswd",
+       "registrar" => "registrar" . $test_domain,
+});
 is( $data->{'response'}, 'failed',     'user/add_remote fails for local domain');
 #TODO: Add tests to verify the data from add_local...
-undef $response; undef $data;
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/add_remote?user=" . $test_username . "\@" . $test_remote_domain . 
-       "&displayname=Automatic testing" .
-       "&password=verysecretpasswd" .
-       "&registrar=registrar" . $test_domain);
+$data = exec_apinode("user/add_remote", {
+       "user" => $test_username . "\@" . $test_remote_domain,
+       "displayname" => "Automatic testing",
+       "password" => "verysecretpasswd",
+       "registrar" => "registrar" . $test_domain,
 
-$data = decode_json( $response->content);
+});
 is( $data->{'response'}, 'ok',         'user/add_remote ok for remote domain');
-#TODO: Add tests to verify the data from add_local...
-undef $response; undef $data;
-
+#TODO: Add tests to verify the data from add_remote...
+undef $data;
 
-$response = $g_ua->get( $api_base . "user/available?user=" . $test_username . "\@" . $test_remote_domain);
-$data = decode_json( $response->content);
+$data = exec_apinode("user/available", { "user" => $test_username . "\@" . $test_remote_domain });
 is( $data->{'response'}, 'failed',             'user/available has been created');
-undef $response; undef $data;
+undef $data;
+
 
-$response = $g_ua->get( $api_base . "user/remove?user=" . $test_username . "\@" . $test_remote_domain);
-$data = decode_json( $response->content);
+$data = exec_apinode("user/remove", { "user" => $test_username . "\@" . $test_remote_domain });
 is( $data->{'response'}, 'ok',                 'user/remove deleted user');
-undef $response; undef $data;
-
-TODO: {
-       local $TODO = 'auth/logout not implemented yet';
-       $response = $g_ua->get( $api_base . "auth/logout");
-       #$data = decode_json( $response->content);
-       is( $data->{'response'}, 'ok',          'auth/logout');
-       undef $response; undef $data;
-}
+undef $data;
+
+logout();
+
index 101431769466154a51dd93381a7c6f445069c81a..a601d77fbff1ce9dbffc2436c5f0de0373e0cf2f 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 require_once('config.php');
+require_once('lib/auth_base.php');
 require_once('lib/user_functions.php');
 require_once('lib/common_functions.php');
 require_once('lib/db_functions.php');
@@ -18,6 +19,7 @@ if ( !$config['sql_link'] )
        print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database connection failed.'));
        exit;
 }
+token_auth();
 
 //*************************************************************************************        
        switch ( $_SERVER['PATH_INFO'] )
index 19dfa06e18f6184c4e8561896a1bf6c6207c56d3..fd8d3b9c0ec165d8ce14a4f54c2058c3f383b9dd 100644 (file)
@@ -1,6 +1,9 @@
+General description on API protocol:
+=========================================================================
 All API nodes currently use GET requests for all parameters.
-All API nodes return JSON data, and all results will contain 
-a 'response' element. The 'response' may be set to:
+All API nodes return JSON data unless explicitly noted , 
+and all JSON results will contain  a 'response' element.
+The 'response' may be set to:
   * 'ok' on success. Further contents depend on the node requested.
   * 'invalid' on invalid requests. A 'cause' element should give a
        keyword denoting what was invalid about the request...
@@ -8,136 +11,492 @@ a 'response' element. The 'response' may be set to:
        failure-keyword (e.g. 'nonexistant', 'inuse', 'dbfail').
        A final 'detail' is commonly included, with a verbose desctiption.
   * 'error' on critical failure. Regard this as a soft form of "500 server error"...
-       
-List of API nodes:
----------------------
 
-user/get?username=foo&domain=bar.bz
-user/get?user=user@bar.bz
+So, in all Return-specifications it should be assumed that 'response' is set to
+'ok' on success.
+
+
+Authentication mechanism:
+=========================================================================
+All API nodes except 'auth/login' and 'auth/logout' require a
+'token authentication' based on a session identifier (session) and a
+session authentication key (auth_key) passed as parameters.
+
+The 'session' and 'auth_key' values are provided as response to a
+successful API login through 'auth/login', and must be included as
+parameters to all subsequent calls to the API.
+
+The 'auth_key' has a limited lifetime (configurabel, defaul 5 minutes),
+significantly shorter than the session lifetime. The 'auth_kye' must
+be renewed before the key lifetime expires, by issuing a call to
+'auth/ping'. The ping call will provide a new key that replaces the
+previous 'auth_key' in subsequent calls to the API.
+
+API login may be done using either a username/password combination,
+or using a registered API key tied to the host originating the requests.
+
+See documentation for nodes under 'auth/*' for information about
+adding, removing and maintaining authentication users and keys.
+
+Sample session:
+
+   call auth/login with username and password
+   store session and auth_key for further requests
+
+   perform API calls with session and auth_key set
+   ...
+
+   call auth/ping with session and auth_key set
+   update the auth_key used for requests
+
+   perform API calls with session and auth_key set
+   ...
+
+   call auth/logout with session set
+
+NOTE: that the API also requires COOKIE-support in the user-agent
+used to communicate with the API.
+
+NOTE: See README for information on configuring API authentication
+backends, and boostrapping the user/key authorizations.
+
+Parameter notes:
+=========================================================================
+ 'user'/'alias' parameters marked '*' may alternatively be given as
+       username=foo + domain=bar
+       alias_username=foo + alias_domain=bar
+
+List of API nodes:
+=========================================================================
+
+auth/login
+-------------------
+  Required:
+       username=authuser
+       password=authpassword
+  Alternate:
+       api_key=API_KEY
+
+  Description:
+       TODO: DOCUMENT THIS.
+
+  Return:
+       Returns 'session' set to the allocated session_name and 'auth_key'
+       set to the generated auth_key. These must be used for further access,
+       and the key must be refreshed through 'auth/ping' at intervals.
+       Returns 'failed' with 'cause' = 'unauthorized' if Login failed
+
+auth/logout
+-------------------
+  Required:
+       session=session_name
+
+  Description:
+       TODO: DOCUMENT THIS.
+       // De-authenticate/deauthorize the ongoing session.
+       // I.e. destroy session data, remove session cookies.
+
+  Return:
+       Returns 'ok' on successful logout
+
+auth/ping
+-------------------
+  Required:
+       session=$session_name
+       auth_key=$auth_key
+
+  Description:
+       TODO: DOCUMENT THIS.
+       // API clients are required to periodically ping the server
+       // The time between pings (interval) is 5 minutes?
+       // A ping call refreshes cookie lifetimes, then
+       // generates and stores a new auth_key
+       // The ping required a valid session...
+       // A successful ping returns a 'response' => 'pong'
+       // along with the new auth_key.
+
+       NOTE!: Does not give 'response' => 'ok' !
+
+  Return:
+       Returns 'pong' with 'auth_key' set to the new key to be used
+       with the session_name in further requests.
+
+auth/new_apikey
+-------------------
+  Required:
+       host_ip=10.20.30.40
+       access=limited_read
+
+  Description:
+       TODO: DOCUMENT THIS.
+       'access' may be one of: 
+         * limited_read
+         * full_read
+         * read_write
+       
+  Return:
+       Returns 'key', 'host' and 'access'.
+       Returns 'invalid' with 'cause' = 'parameters' on parameter error
+       Returns 'failed' with  'cause' = 'error' on database errors.
+       TODO: Change 'error' to 'dbfail'.
+
+auth/remove_apikey
+-------------------
+  Required:
+       api_key=$key
+
+  Description:
+       TODO: DOCUMENT THIS.
+
+  Return:
+       Returns 'key' set to the removed key on success.
+       Returns 'invalid' on parameter error
+       Returns 'failed' with 'cause' = 'nonexistant' if key does not exist.
+       Returns 'failed' with 'cause' = 'error' on database errors.
+       TODO: Change 'error' to 'dbfail'.
+
+auth/list_apikeys
+-------------------
+
+  Description:
+       TODO: DOCUMENT THIS.
+
+  Return:
+       Returns 'list'
+
+auth/authorize_user
+-------------------
+  Required:
+       username=authuser
+       access=limited_read
+
+  Description:
+       TODO: DOCUMENT THIS.
+       // Add or update a valid back-end user in authorization
+       // if the current authentication has write access.
+       // If the authorization does not exist, add it.
+       // If the user is already authorized, replace access level.
+
+       'access' may be one of: 
+         * limited_read
+         * full_read
+         * read_write
+       
+  Return:
+       Returns 'user' and 'access' when user was successfully added.
+       Returns 'invalid' with 'cause' = 'parameters' on parameter error
+       Returns 'failed' with 'cause' = 'nonexistant' if user does not exist.
+       Returns 'failed' with 'cause' = 'error' on database errors.
+
+auth/remove_user
+-------------------
+  Required:
+       username=authuser
+
+  Description:
+       TODO: DOCUMENT THIS.
+       // If the current authentication has write access:
+       // Remove authorization for the given users.
+       // Delete user from backend if backend is read-write.
+
+  Return:
+       Returns 'user' when user was successfully removed.
+       Returns 'invalid' with 'cause' = 'parameters' on parameter error
+       Returns 'failed' with 'cause' = 'nonexistant' if user does not exist.
+       Returns 'failed' with 'cause' = 'error' on database errors.
+
+auth/list_users
+-------------------
+
+  Description:
+       Returns 'list'
+
+auth/add_user
+-------------------
+
+  Description:
+       TODO: This is not implemented. Document, write test and implement.
+       Returns 'notimplemented'
+       // Add user to backend if backend is read-write and
+       // the current authentication has write access.
+       // The created user should be added to authorizations
+       // with an access level of "limited_read (1)"
+
+auth/update_user
+-------------------
+
+  Description:
+       TODO: This is not implemented. Document, write test and implement.
+       Returns 'notimplemented'
+       // Update the given user in the backend, if the backend
+       // is read-write, and the current authentication has
+       // write access.
+
+user/get
+-------------------
+  Required:
+       user=user@bar.bz *
+    
+  Description:
        Return User information, not including hardphone association.
-       Possible extension: allow user/get?address=foo@bar
+       
+  Return:
+       On success, 'user' will be set to the user-data set:
+       for all users:
+               type,username,password,domain,authid,registrar,
+               r_port,proxy,p_port,dialplan,displayname,linetext
+       for local users (kamailio), as above, plus:
+               email,ha1,ha1b,rpid,permittedcalls
+       Returns 'failed' with 'cause' = 'nonexistant' if user does not exist..
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
 
 user/list
-user/list?search=foo@bar
+-------------------
+  Optional:
+       search=foo@bar
+
+  Description:
        Return an array of users, with 'user' set to SIP-address and
        'displayname' set to the users Displayname.
 
        If the search parameter is included, a globbing search is performed,
        and only matches are listed.
 
-user/add_local?username=foo&domain=bar.bz&displayname=baz&email=qux@zef.tld
-user/add_local?user=foo@bar.bz&displayname=baz&email=qux@zef.tld
+  Return:
+       TODO: Document this
+
+user/add_local
+-------------------
+  Required:
+       user=foo@bar.bz *
+       displayname=baz
+       email=qux@zef.tld
+
+  Description:
        Adds a user account to both Kamailio and provisioning, if the
        username@domain is nonexistant, and the domain is local (handled by
        Kamailio). The password for the user is auto-generated.
 
+  Return:
        Returns a full user object, the same form as user/get
-
-user/add_remote?username=foo&domain=bar&password=S3cr3t&displayname=baz&registrar=sip.example.com
-user/add_remote?user=foo@bar.bz&password=S3cr3t&displayname=baz&registrar=sip.example.com
-  optionals: &r_port=5060&proxy=sipproxy.example.com&p_port=5060&authid=realfoo&dialplan=(.*)&linetext=lalala
+       Returns 'failed' with 'cause' = 'exists' if address is taken
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
+       Returns 'failed' with 'cause' = 'servfail' if no default servers
+       could be determined for the requested domain. See domain/set_servers.
+       Returns 'failed' with 'cause' = 'dbfail' on database errors..
+
+user/add_remote
+-------------------
+  Required:
+       user=foo@bar.bz *
+       password=S3cr3t
+       displayname=baz
+       registrar=sip.example.com
+  Optional: 
+       r_port=5060
+       proxy=sipproxy.example.com
+       p_port=5060
+       authid=realfoo
+       dialplan=(.*)
+       linetext=lalala
+
+  Description:
        Adds a provisioning user for a remote SIP account. This allows locally
        provisioned hardphones to be associated with non-local SIP accounts.
 
+  Return:
        Returns a full user object, the same form as user/get
-       
        Returns 'failed' with  'cause' = 'domain' if an attempt is made to
        add_remote for a local domain.
+       Returns 'failed' with 'cause' = 'exists' on any address collisions
+       Returns 'failed' with 'cause' = 'dbfail' on database errors..
+
+user/remove
+-------------------
+  Required:
+       user=foo@bar.bz *
+
+  Description:
+       Removes user account from Kamailio, if present, and removes user from
+       provisioning.  
+
+       Will fail if user has associated hardphones, remove phones before
+       removing user. 
 
-user/change_pw?username=foo&domain=bar&password=baz
-user/change_pw?user=foo@bar.bz&password=baz
+       TODO: Should fail if user has associated aliases. Remove aliases before
+       removing user.
+
+  Return:
+       TODO: Document additional data on 'result' = 'ok'
+       Returns 'failed' with 'cause' = 'nonexistant' if user does not exist..
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
+       Returns 'failed' with 'cause' = 'inuse' if phone-associations exists
+       TODO: Returns 'failed' with 'cause' = 'inuse' if aliases exist for the address
+
+
+user/change_pw
+-------------------
+  Required:
+       user=foo@bar.bz *
+       password=baz
+
+  Description:
        Changes the password for the given user, returns 'ok' with 'detail' as
        a descriptive text on success. In the current implementation,
        provisioning and kamailio passwords are handled separately, so one
        may succeed and the other fail. As is, this permits password changes
        of non-local provision users with minimal effort.
 
+  Return:
+       Returns 'detail' on success.
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
        Returns 'failed' with 'cause' = 'nonexistant' if user does not exist.
-       
        Returns 'failed' with 'cause' = 'dbfail' if the database request
        failed. Note that this may be because the given password was identical
        to the old.
 
-user/update?username=foo&domain=bar
-user/update?user=foo@bar.bz
-  optionals: &displayname=baz&dialplan=(.*)&linetext=lalala&email=foo@bar.baz
-       Not implemented! Will only be implemented for Local users.
-       Remote users will have to be removed, and re-added.
+user/change_email
+-------------------
+  Required:
+       user=foo@bar.bz *
+       email=user@example.com
 
-user/change_email?username=foo&domain=bar&email=user@example.com
-user/change_email?user=foo@bar.bz&email=user@example.com
+  Description:
        Changes the email address for the given user, returns 'ok' with
        'user' set to the requested username and 'email' set to the
        email address.  This only applies to kamailio local users.
 
+  Return:
+       Returns 'user' and 'email' on success.
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
+       TODO: Returns 'failed' with 'cause' = 'nonexistant' if user does not exist.
+       Returns 'failed' with 'cause' = 'dbfail' if the database request
+       failed. This may be because no update was required ...
+
+user/update
+-------------------
+  Required:
+       user=foo@bar.bz *
+  optionals: 
+       displayname=baz
+       dialplan=(.*)
+       linetext=lalala
+       email=foo@bar.baz
+
+       Updates user with the given data....
+       TODO: Extend this description ;)
+
+  Return:
+       Returns two arrays on success, 'updated' containing updated params
+       and 'skipped' containing specified parameters that did not require
+       any changes.
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
        Returns 'failed' with 'cause' = 'nonexistant' if user does not exist.
-       
        Returns 'failed' with 'cause' = 'dbfail' if the database request
-       failed. Note that this may be because the given password was identical
-       to the old.
-
+       failed. With database errors, the array 'failed' will contain any
+       parameters that may have caused the dbfail.
 
-user/remove?username=foo&domain=bar
-user/remove?user=foo@bar.bz
-       Removes user account from Kamailio, if present, and removes user from
-       provisioning.  
+user/available
+-------------------
+       user=foo@bar.bz *
 
-       Will fail if user has associated hardphones, remove phones before
-       removing user. 
-
-       TODO: Should fail if user has associated aliases. Remove aliases before
-       removing user.
-
-       Returns 'ok' on success.
-
-user/available?username=foo&domain=bar
-user/available?user=foo@bar.bz
+  Description:
        Tests an address to see if it is available.
 
+  Return:
        Returns 'ok' with 'cause' = 'nonexistant' if the address is available.
-
        Returns 'failed' with 'cause' = 'exists' if the address is in use.
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
 
 user/gen_pw
+-------------------
+
+  Description:
        Generates a random password on the same form as that used by
        user/add_local. 
 
+       NOTE: This node does not use JSON formatting.
+
+  Return:
        Return is always a password in plain text, with no formatting.
 
-phone/get?mac=f00ba2ba5c00
-phone/get?user=foo@bar.bz
-phone/get?username=foo&domain=bar.bz
+phone/get
+-------------------
+  Optional:
+       mac=f00ba2ba5c00
+       user=foo@bar.bz *
+
+  Description:
        Returns a list of associated elements. If MAC address is given as
        parameter, a list of associated user@domain addresses is retruned. If
        user@domain is given as parameter, a list of associated mac addresses
        is returned.
 
-phone/list?search=f00ba2
+  Return:
+       Returns 'list' as an array of phone-user mappings on success,
+       where each element of 'list' consists of either usernames or mac-adresses.
+       Returns 'failed' with 'cause' = 'nonexistant' on no matches.
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
+
+phone/list
+-------------------
+  Optional:
+       search=f00ba2
+
+  Description:
        Returns the list of registered MAC addresses. If the (optional) search
        parameter is included, the list is the result of a globbing search,
        a subset matching on the MAC addresses.
 
-phone/add?mac=f00ba2ba5c00&username=foo&domain=bar.bz
-phone/add?mac=f00ba2ba5c00&user=foo@bar.bz
+  Return:
+       TODO: Document this.
+
+phone/add
+-------------------
+  Required:
+       user=foo@bar.bz *
+       mac=f00ba2ba5c00
+
+  Description:
        Adds a phone-to-user association to Provisioning. The user may be
        provided as 'username' and 'domain' in separate parameters, or as
        'user' in sip-address form. The MAC address must be valid, in plain-hex
        or colon-separated form, and the user must match a valid user
        registration.
 
-       Returns the mac+user combinations on success.
+  Return:
+       Returns 'mac', 'username' and 'domain' on success.
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
+       Returns 'failed' with 'cause' = 'invalid' if MAC address is malformed.
+       Returns 'failed' with 'cause' = 'nonexistant' if user does not exist.
+       Returns 'failed' with 'cause' = 'exists' if user+mac is already reg.
+       Returns 'failed' with 'cause' = 'dbfail' on database failure.
+
+phone/remove
+-------------------
+  Required:
+       user=foo@bar.bz *
+       mac=f00ba2ba5c00
 
-phone/remove?mac=f00ba2ba5c00&username=foo&domain=bar.bz
-phone/remove?mac=f00ba2ba5c00&user=foo@bar.bz
+  Description:
        Deletes a phone-to-user association. Required parameters and formats
        are identical to the phone/add node.
-       Returns 'ok' on success.
+
+  Return:
+       TODO: Document additional info provided on 'result' = 'ok'
+       Returns 'failed' with 'cause' = 'invalid' if SIP address is malformed.
+       Returns 'failed' with 'cause' = 'invalid' if MAC address is malformed.
+       Returns 'failed' with 'cause' = 'nonexistant' on no such registration.
 
 
 numbers/list
-numbers/list?search=1234
-numbers/list?random=true
-  optional: limit=n
+-------------------
+  Optional:
+       search=1234
+       random=true
+       limit=n
+
+  Description:
        Gives a list of entries in the phone number pool.
        With no parameters, the entire (ordered) list of available numbers is
        returned.  With 'search', the result of a pre-post globbing search is
@@ -145,118 +504,186 @@ numbers/list?random=true
        order) For all nodes, 'limit' may be a numeric limit of desired
        results.
 
-numbers/add_range?start=%2B471234&end=%2B47123456
+  Return:
+       Returns the list of numbers as the array 'list'.
+       Returns 'failed' with 'cause' = 'empty' if no numbers were listed...
+
+numbers/add_range
+-------------------
+  Required:
+       start=+4712345000
+       end=+4712345678
+
+  Description:
        The start and end must have identical lead-in, and start must be less
        than end.  Both numbers must be given in E164 format, remember to
        urlencode the plus (+ -> %2B). The numeric component of both will be
        extracted, and all numbers in the range will be iteratively added to
        the pool, including the ending number.
-       Returns 'ok' on success.
 
-numbers/add?number=%2B4761123456
+  Return:
+       TODO: Document additional info provided on 'result' = 'ok'
+       Returns 'failed' with 'cause' = 'rejected' with 'detail' set on failure
+
+numbers/add
+-------------------
+  Required:
+       number=+4712345679
+
+  Description:
        Adds the given number to the pool, after verifying that the number is
        in valid E164 format, and that the number does not already exist in the
        pool.
 
-       Returns 'response' 'ok' and "Added <number>" in 'detail' on success.
-
-numbers/remove?number=%2B4761123456
+  Return:
+       Returns "Added <number>" in 'detail' on success.
+       Returns 'failed' with 'cause' = 'rejected' and 'detail' set to "Not a
+       valid E164 number" if the provided number could not pass E164
+       validation.
+       Returns 'failed' with 'cause' = 'exists' if number is already in the pool.
+       TODO: Returns 'failed' with 'cause' = 'dbfail' on database failure.
+       TODO: Gives 'cause' = 'rejected' with no detail in current code.
+
+numbers/remove
+-------------------
+  Required:
+       number=+4712345676
+
+  Description:
        Removes/pulls a number from the pool.
        The number must be a valid e164 number, and must be present in the
        pool.
 
-       Returns 'response' as 'ok' with 'number' set to the number that was
-       pulled from the pool.
+  Return:
+       Returns 'number' set to the number that was pulled from the pool.
+       Returns 'failed' with 'cause' = 'rejected' and 'detail' set to "Not a
+       valid E164 number" if the provided number could not pass E164
+       validation.
+       Returns 'failed' with 'cause' = 'nonexistant' if number is not the pool.
+       Returns 'failed' with 'cause' = 'dbfail' on database failure.
+
+numbers/inpool
+-------------------
+  Required:
+       number=+4712345676
 
-numbers/inpool?number=%2B4761123456
+  Description:
        Tests if the given E164 number is in the pool.
-       Returns 'response' = 'ok' with 'number' = '<number>' If the number is
-       in the pool
 
+  Return:
+       Returns 'number' = '<number>' If the number is in the pool
        Returns response = 'failed' with 'cause' = 'nonexistant' if not.
 
 alias/list
-alias/list?destination=foo@bar.bz
-alias/list?destination=foo@bar.bz&e164=true
-alias/list?alias=foo@bar.bz
+-------------------
+  Optional:
+       alias=foo@bar.bz
+       destination=foo@bar.bz
+       e164=true
+
+  Description:
        With no parameters, this will return all defined aliases (potentially a
-       huge list). With the destination parameter set, only aliases dfor that
+       huge list). With the destination parameter set, only aliases for that
        destination will be listed, and with the e164 option set to true, only
        an e164 alias will be returned (if one/it exists). The alias parameter
-       gives the same behaviour, butr looks up an alias address instead of the
+       gives the same behaviour, but looks up an alias address instead of the
        destination. The e164 option is not valid for the alias search
        (naturally).
 
+  Return:
        Returns 'ok' on success, with an array of 'destination' and 'alias'
        pairs.
-
        Returns 'ok' with an empty array if the search gave no results.
-
        Returns 'ok' with an empty array if the database search fails.
-
        Returns 'failed' with 'cause' = 'invalid' on invalid SIP addresses.
 
-alias/add?alias_username=foo&alias_domain=bar.bz&destination=bar@qux.zx
-alias/add?alias=foo@bar.bz&destination=bar@qux.zx
+alias/add
+-------------------
+  Required:
+       alias=foo@bar.bz *
+       destination=bar@qux.zx
+
+  Description:
        Add an alias specified by alias_username and alias_domain that
        points to the destination SIP-adress. 
 
+  Return:
        On success, 'ok' is returned, with 'alias' and 'destination' set to the
        resulting alias and destination adresses.
-
        Returns 'invalid' with 'cause' = 'destination' or 'alias' if the
        resulting alias-adress or destination are invalid SIP-adresses.
-
        Returns 'failed' with 'cause' = 'nxdomain' if the given alias domain is
        not a Kamailio domain.
-
        Returns 'failed' with 'cause' = 'exists' if the alias is an E164 number
        and the user already has an E164 alias registered.
-
        Returns 'failed' with 'cause' = 'nonexistant' when an alias for a local
        domain is requested, but the local subscriber does not exist.
-
        Returns 'failed' with 'cause' = 'exists' for aliases that already
        exists, (and aliases that overlaps SIP-accounts - not implemented).
 
 
-alias/remove?alias_username=foo&alias_domain=bar.bz
-alias/remove?alias=foo@bar.bz
+alias/remove
+-------------------
+  Required:
+       alias=foo@bar.bz *
+
+  Description:
        Removes the alias given by alias_username and alias_domain.
 
+  Return:
        Returns 'ok' with the removed alias adress as 'alias' on success
-
        Returns 'failed' with 'cause' = 'nonexistant' it the alias does not
        exists.
-
        Returns 'invalid' with 'cause' = 'address' if the given alias is not a
        valid SIP adress.
+
+
 domain/list
+-------------------
+
+  Description:
        Returns a list of configured and valid domains. May return an empty
        list if no domains are configured for kamailio. No node is provided
        to add domains, this is not a task for Hermes, but a kamailio
        configuration task.
 
+  Return:
+       Returns a list of domains as 'list' on success.
        Returns 'failed' on database errors.
 
-domain/get_servers?domain=bar.bz
+domain/get_servers
+-------------------
+  Required:
+       domain=bar.bz
+
+  Description:
        Provides the default registrar/proxy/provisioning server information
        for the given domain.
 
+  Return:
        Returns 'ok' with 'servers' set to a kvp-set on success, containing:
-       'registrar', 'r_port', 'proxy', 'p_port', 'prov_url'.
-
+       'registrar','r_port','proxy','p_port','prov_url'.
        Returns 'failed' with 'cause' = 'nonexistant' if lookup of data for
        given domain results in an empty set (not configured).
 
-domain/set_servers?domain=bar.bz&registrar=server.bar.bz&r_port=5060&proxy=proxy.bar.bz&p_port=5060&prov_url=http://server.bar.bz/hermes/prov
+domain/set_servers
+-------------------
+  Required:
+       domain=bar.bz
+       registrar=server.bar.bz
+       r_port=5060
+       proxy=proxy.bar.bz
+       p_port=5060
+       prov_url=http://server.bar.bz/hermes/prov
+
+  Description:
        Sets the server data for the given domain. All of these parameters
-       are required: 'domain', 'registrar', 'r_port', 'proxy', 'p_port', 'prov_url'.
-       If no defaul server data is defined, the configuration is added.
+       are Required: 'domain', 'registrar', 'r_port', 'proxy', 'p_port', 'prov_url'.
+       If no default server data is defined, the configuration is added.
        If server configuration existed, the default data is updated.
 
+  Return:
        Returns 'ok' with 'servers' set as domain/get_servers on success.
-
        Returns 'failed' with 'cause' set to 'cause' set to 'parameters'
        if one of these is true:
                * Missing parameters
@@ -264,26 +691,21 @@ domain/set_servers?domain=bar.bz&registrar=server.bar.bz&r_port=5060&proxy=proxy
                * Non-numeric values for port-numbers.
        If you need to determine which of these triggered, the
        'description' contains a text describing the actual fail.
-
        Return 'failed' with 'cause' = 'error' on database failure.
 
 BUGS:
 ---------------------
 
-api/alias/list?destination=foo@bar.bz&e164=true
+api/alias/list destination=foo@bar.bz e164=true
        should return an empty array, returns false
 
 TODO list:
 ---------------------
 
-Add a node to test a destination (pre-flight testing of user/add and alias/add)
+Some TODO's listed above ...
 
-Read list of kamailio domains (domains/list ?) RW?
-Default server-settings for domains (domains/server ? ) (registrar, proxy, ports...) RW.
 Permissions! (user/permissions?user=...)
 
-authentication-mechanism :)
-
 Change all GET to POST
 
 Implement test-tool for POST-based communication ;)
@@ -291,3 +713,7 @@ Implement test-tool for POST-based communication ;)
 Improve robustness of change_pw: fetch old password for rollback/testing.
 
 Check for locations where sql_dbexec_rows is more appropriate/correct than sql_dbexec
+
+Perform an evaluation of the security related to the api-key functionality,
+and replace if the method is evaluated as too weak. I.e. implement strong
+non-interactive, non-user based authentication.
index eaf4260fb6a58b48420074b2a059eafbdc64b3c0..b4555a12c2f500610d9cc19d1229d0ff4d9c4b18 100644 (file)
@@ -1023,6 +1023,35 @@ CREATE DATABASE /*!32312 IF NOT EXISTS*/ `provision` /*!40100 DEFAULT CHARACTER
 
 USE `provision`;
 
+--
+-- Table structure for table `apikeys`
+--
+
+CREATE TABLE IF NOT EXISTS `apikeys` (
+  `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `host` varchar(64) NOT NULL,
+  `apikey` varchar(128) NOT NULL,
+  PRIMARY KEY  (`apikey`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `authorizations`
+--
+
+DROP TABLE IF EXISTS `authorizations`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE IF NOT EXISTS `authorizations` (
+  `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `authid` varchar(255) NOT NULL,
+  `type` varchar(16) NOT NULL,
+  `access_level` int(11) NOT NULL,
+  PRIMARY KEY  (`authid`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
 --
 -- Table structure for table `number_pool`
 --
@@ -1074,7 +1103,23 @@ CREATE TABLE `servers` (
 ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
 SET character_set_client = @saved_cs_client;
 
+--
+-- Table structure for table `sessionkeys`
+--
 
+DROP TABLE IF EXISTS `sessionkeys`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `sessionkeys` (
+  `sessid` varchar(255) NOT NULL,
+  `session` varchar(128) NOT NULL,
+  `authid` varchar(128) NOT NULL,
+  `client` varchar(255) NOT NULL,
+  `key` varchar(128) NOT NULL,
+  `last` datetime default NULL,
+  PRIMARY KEY  (`session`,`authid`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
 
 --
 -- Table structure for table `users`