All functionality in README implemented and working.

This commit is contained in:
Paul Banks 2010-01-10 13:57:23 +00:00
parent fb19751d51
commit d05b6572bb
10 changed files with 827 additions and 9 deletions

19
LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2010 Paul Banks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -57,7 +57,7 @@ This means:
### Sprig and other ORMs
AACL ships with a Sprig based Rule model and Sprig based class for easily turning Sprig models into Access Controlled Resources.
It should be relatively trivial to override these with ORM (or other library) specific versions and be able to use all core functionality.
It should be relatively trivial to modify the library to work with other ORMs but as this is only likely to be used by me for now, a flexible driver system seems unnecessary.
### Concept: ACL Resources
@ -68,22 +68,22 @@ against which access rules can be created.
#### AACL_Resource Interface
The `AACL_Resource` interface defines three methods:
The `AACL_Resource` interface defines four methods:
- **acl_id()**
- **public function acl_id()**
Must return a string that uniquely identifies the current object as a resource.
Convention is for controllers to return as `c:controller_name` and models as `m:model_name.primary_key_value`
Remember that dot in model identifier - it is significant!
- **acl_actions($return_current = FALSE)**
- **public function acl_actions($return_current = FALSE)**
This method servers a dual purpose. When the argument `$return_current` is false, the method should return an array of string names, one for each action
that can be carried out on the resource. For no specific actions, an empty array should be returned.
- `Controller_AACL` returns an array containing the names of all public action methods automatically.
- `Sprig_AACL` returns actions 'create', 'read', 'update', 'delete'. These can be changed by overriding this method in specific models.
- **acl_conditions(Model_User $user = NULL, $condition = NULL)**
- **public function acl_conditions(Model_User $user = NULL, $condition = NULL)**
This method also servers a dual purpose: it both defines available conditions and checks them.
@ -92,6 +92,16 @@ The `AACL_Resource` interface defines three methods:
- When a user object and condition id are passed, the funtion should return a boolean indicating whether or not the condition has passed.
- **public static function acl_instance($class_name)**
This method is used for auto-discovery of available resources. Since the resource ID, actions and conditions must be obtained from an object,
we need a way to get and instance of the object given only the class name. Not that the object returned shoul not be used for anything except calling `acl_*` methods
to discover resource properties.
Note that the `Model_AACL_Rule` itself extends `Sprig_AACL`. Instead of the default CRUD actions though it just specifies `grant` and `revoke` actions. This means you can
create rules about whether a role can itsef grant or revoke access! Note that the checking is not automatic though. That would prevent installers from creating rules or similar
due to not having a user logged in yet! It is still up to the developer to check the user has permission to grant or revoke using check().
### Resource Conditions
`AACL_Resource` objects can define conditions which allow rules to provide fine-grained control. Since conditions are resource specific, only conditions defined by the resource
@ -198,13 +208,17 @@ One of the key requirements for this library is to make checking access rights a
All checking is done using `AACL::check()` described below:
**AACL::check($resource, $action = NULL)**
**AACL::check(AACL_Resource $resource, $action = NULL)**
- **$resource**
Either a string resource ID or an AACL_Resource object. If an object is passed, `check()` will attempt to get the current action from the resource automatically
The AACL_Resource being requested. `check()` will attempt to get the current action from the resource automatically
using `$reource->acl_actions(TRUE)`. If this returns a string action then that action will be used for checking without having to specify the `$action` parameter.
Note that the string resource ID can't be specified since the `check()` function requires aaccess to the objects acl_* methods. Even if a method of mapping IDs to objects was
implemented, there are issues creating instances of controllers and working out which URI to specify etc. This means that currently there is no way to check permisions on a
controller resource other than the one in which the call to `AACL::check()` resides. In practice this is unlikely to be a real limitiation.
This means that, since a controller object knows the currently executing action, the current controller action can be checked simply with `AACL::check($this)`.
Since models don't inherently know which action is being requested, `$action` parameter must be specified (or permission to access all actions will be required).
@ -269,4 +283,4 @@ for `m:post` rather than `m:post.1, m:post.2, ...`. It is left for the developer
### UI
A basic rule management UI will hoepfully be added to the module at some point to help get started. It will naturally be disabled in all but 'developement' environment.
A basic rule management UI will hopefully be added to the module at some point to help get started. It will naturally be disabled in all but 'developement' environment.

292
classes/aacl.php Normal file
View File

@ -0,0 +1,292 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* Another ACL
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
class AACL
{
/**
* All rules that apply to the currently logged in user
*
* @var array contains Model_AACL_Rule objects
*/
protected static $_rules;
/**
* Grant access to $role for resource
*
* @param mixed string role name or Model_Role object
* @param string resource identifier
* @param string action [optional]
* @param string condition [optional]
* @return void
*/
public static function grant($role, $resource, $action = NULL, $condition = NULL)
{
// Normalise $role
if ( ! $role instanceof Model_Role)
{
$role = Sprig::factory('role', array('name' => $role))->load();
}
// Check role exists
if ( ! $role->loaded())
{
throw new AACL_Exception('Unknown role :role passed to AACL::grant()',
array(':role' => $role->name));
}
// Create rule
Sprig::factory('aacl_rule', array(
'role' => $role->id,
'resource' => $resource,
'action' => $action,
'condition' => $condition,
))->create();
}
/**
* Revoke access to $role for resource
*
* @param mixed string role name or Model_Role object
* @param string resource identifier
* @param string action [optional]
* @param string condition [optional]
* @return void
*/
public static function revoke($role, $resource, $action = NULL, $condition = NULL)
{
// Normalise $role
if ( ! $role instanceof Model_Role)
{
$role = Sprig::factory('role', array('name' => $role))->load();
}
// Check role exists
if ( ! $role->loaded())
{
// Just return without deleting anything
return;
}
$model = Sprig::factory('aacl_rule', array(
'role' => $role->id,
));
if ($resource !== '*')
{
// Add normal reources, resource '*' will delete all rules for this role
$model->resource = $resource;
}
if ($resource !== '*' AND ! is_null($action))
{
$model->action = $action;
}
if ($resource !== '*' AND ! is_null($condition))
{
$model->condition = $condition;
}
// Delete rule
$model->delete();
}
/**
* Checks user has permission to access resource
*
* @param AACL_Resource AACL_Resource object being requested
* @param string action identifier [optional]
* @throw AACL_Exception To identify permission or authentication failure
* @return void
*/
public static function check(AACL_Resource $resource, $action = NULL)
{
if ($user = Auth::instance()->get_user())
{
// User is logged in, check rules
$rules = self::_get_rules($user);
foreach ($rules as $rule)
{
if ($rule->allows_access_to($resource, $action))
{
// Access granted, just return
return;
}
}
// No access rule matched
throw new AACL_Exception_403;
}
else
{
// User is not logged in and the need to be
throw new AACL_Exception_401;
}
}
/**
* Get all rules that apply to user
*
* @param Model_User $user
* @param bool [optional] Force reload from DB default FALSE
* @return array
*/
protected static function _get_rules(Model_User $user, $force_load = FALSE)
{
if ( ! isset(self::$_rules) OR $force_load)
{
// Get rule model instance
$model = Sprig::factory('aacl_rule');
self::$_rules = Sprig::factory('aacl_rule')
->load(DB::select()
// Select all rules that apply to any of the user's roles
->where($model->field('role')->column, 'IN', $user->roles->as_array(NULL, 'id'))
// Order by resource length as this will mostly mean that
// Less specific rules come first making the checking quicker
->order_by('LENGTH("'.$model->field('resource')->column.'")', 'ASC')
, FALSE)->as_array();
}
return self::$_rules;
}
protected static $_resources;
/**
* Returns a list of all valid resource objects based on the filesstem adn reflection
*
* @param mixed string resource_id [optional] if provided, the info for that specific resource ID is returned, if TRUE a flat array of just the ids is returned
* @return array
*/
public static function list_resources($resource_id = FALSE)
{
if ( ! isset(self::$_resources))
{
// Find all classes in the application and modules
$classes = self::_list_classes();
// Loop throuch classes and see if they implement AACL_Resource
foreach ($classes as $i => $class_name)
{
$class = new ReflectionClass($class_name);
if ($class->implementsInterface('AACL_Resource'))
{
// Ignore interfaces
if ($class->isInterface())
{
continue;
}
// Ignore abstract classes
if ($class->isAbstract())
{
continue;
}
// Create an instance of the class
$resource = $class->getMethod('acl_instance')->invoke($class_name, $class_name);
// Get resource info
self::$_resources[$resource->acl_id()] = array(
'actions' => $resource->acl_actions(),
'conditions' => $resource->acl_conditions(),
);
}
unset($class);
}
}
if ($resource_id === TRUE)
{
return array_keys(self::$_resources);
}
elseif ($resource_id)
{
return isset(self::$_resources[$resource_id]) ? self::$_resources[$resource_id] : NULL;
}
return self::$_resources;
}
protected static function _list_classes($files = NULL)
{
if (is_null($files))
{
// Remove core module paths form search
$loaded_modules = Kohana::modules();
$exclude_modules = array('database', 'orm', 'sprig', 'auth', 'sprig-auth',
'userguide', 'image', 'codebench', 'unittest', 'pagination');
$paths = Kohana::include_paths();
// Remove known core module paths
foreach ($loaded_modules as $module => $path)
{
if (in_array($module, $exclude_modules))
{
unset($paths[array_search($path.DIRECTORY_SEPARATOR, $paths)]);
}
}
// Remove system path
unset($paths[array_search(SYSPATH, $paths)]);
$files = Kohana::list_files('classes', $paths);
}
$classes = array();
foreach ($files as $name => $path)
{
if (is_array($path))
{
$classes = array_merge($classes, self::_list_classes($path));
}
else
{
// Strip 'classes/' off start
$name = substr($name, 8);
// Strip '.php' off end
$name = substr($name, 0, 0 - strlen(EXT));
// Convert to class name
$classes[] = str_replace(DIRECTORY_SEPARATOR, '_', $name);
}
}
return $classes;
}
/**
* Force static access
*
* @return void
*/
protected function __construct() {}
/**
* Force static access
*
* @return void
*/
protected function __clone() {}
} // End AACL

View File

@ -0,0 +1,14 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* Base AACL exception
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
class AACL_Exception extends Kohana_Exception {}

View File

@ -0,0 +1,20 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* 401 "User requires authentication" exception
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
class AACL_Exception_401 extends AACL_Exception
{
public function __construct()
{
parent::__construct('Authentication Required');
}
}

View File

@ -0,0 +1,20 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* 403 "Permission denied" exception
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
class AACL_Exception_403 extends AACL_Exception
{
public function __construct()
{
parent::__construct('Permission Denied');
}
}

65
classes/aacl/resource.php Normal file
View File

@ -0,0 +1,65 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* AACL Resource interface
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
interface AACL_Resource
{
/**
* Gets a unique ID string for this resource
*
* Convention for controllers is c:controller_name
* Convention for models is m:model_name.primary_key_value
*
* @return string
*/
public function acl_id();
/**
* Returns actions specific to this resource as an array
*
* For no actions return empty array
*
* Example: return array('create', 'read', 'update', 'delete')
*
* If $return_current is TRUE, return value should be the currently requested action or NULL if not known.
*
* @param bool $return_current [optional]
* @return mixed
*/
public function acl_actions($return_current = FALSE);
/**
* Defines any condition fro this resource
*
* If params are provided, returns a boolean indicating whether $user meets condition specified in $condition
*
* If no params are provided, returns an array or available conditions for this resource in form
*
* return array('condition_id' => 'User friendly description of condition');
*
* @param Model_User $user [optional] logged in user model
* @param object $condition [optional] condition to test
* @return mixed
*/
public function acl_conditions(Model_User $user = NULL, $condition = NULL);
/**
* Returns an instance of the current object suitable for calling the above methods
*
* Note that the object instance returned should not be used for anything except querying the acl_* methods
*
* @param string Class name of object required
* @return Object
*/
public static function acl_instance($class_name);
} // End AACL_Resource

104
classes/controller/aacl.php Normal file
View File

@ -0,0 +1,104 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* Base class for access controlled controllers
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
abstract class Controller_AACL extends Controller_Template implements AACL_Resource
{
/**
* AACL_Resource::acl_id() implementation
*
* @return string
*/
public function acl_id()
{
// Controller namespace, controller name
return 'c:'.strtolower($this->request->controller);
}
/**
* AACL_Resource::acl_actions() implementation
*
* @param bool $return_current [optional]
* @return mixed
*/
public function acl_actions($return_current = FALSE)
{
if ($return_current)
{
return $this->request->action;
}
// Find all actions in this class
$reflection = new ReflectionClass($this);
$actions = array();
// Add all public methods that start with 'action_'
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
{
if (substr($method->name, 0, 7) === 'action_')
{
$actions[] = substr($method->name, 7);
}
}
return $actions;
}
/**
* AACL_Resource::acl_conditions() implementation
*
* @param Model_User $user [optional] logged in user model
* @param object $condition [optional] condition to test
* @return mixed
*/
public function acl_conditions(Model_User $user = NULL, $condition = NULL)
{
if (is_null($user) AND is_null($condition))
{
// We have no conditions
return array();
}
else
{
// We have no conditions so this test should fail!
return FALSE;
}
}
/**
* AACL_Resource::acl_instance() implementation
*
* Note that the object instance returned should not be used for anything except querying the acl_* methods
*
* @param string Class name of object required
* @return Object
*/
public static function acl_instance($class_name)
{
// Return controller instance populated with manipulated request details
$instance = new $class_name(Request::instance());
$controller_name = strtolower(substr($class_name, 11));
if ($controller_name !== Request::instance()->controller)
{
// Manually override controller name and action
$instance->request->controller = strtolower(substr(get_class($this), 11));
$instance->request->action = NULL;
}
return $instance;
}
} // End Controller_AACL

169
classes/model/aacl/rule.php Normal file
View File

@ -0,0 +1,169 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* Access rule model
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
class Model_AACL_Rule extends Sprig_AACL
{
protected function _init()
{
$this->_fields += array(
'id' => new Sprig_Field_Auto,
'role' => new Sprig_Field_BelongsTo(array(
'model' => 'role',
)),
'resource' => new Sprig_Field_Char(array(
'max_length' => 45,
'null' => FALSE,
)),
'action' => new Sprig_Field_Char(array(
'max_length' => 25,
'null' => TRUE,
)),
'condition' => new Sprig_Field_Char(array(
'max_length' => 25,
'null' => TRUE,
)),
);
}
/**
* Check if rule matches current request
*
* @param AACL_Resource AACL_Resource object that user requested access to
* @param string action requested [optional]
* @return
*/
public function allows_access_to(AACL_Resource $resource, $action = NULL)
{
if ($this->resource === '*')
{
// No point checking anything else!
return TRUE;
}
if (is_null($action))
{
// Check to see if Resource whats to define it's own action
$action = $resource->acl_actions(TRUE);
}
// Get string id
$resource_id = $resource->acl_id();
// Make sure action matches
if ( ! is_null($action) AND ! is_null($this->action) AND $action !== $this->action)
{
// This rule has a specific action and it doesn't match the specific one passed
return FALSE;
}
$matches = FALSE;
// Make sure rule resource is the same as requested resource, or is an ancestor
while( ! $matches)
{
// Attempt match
if ($this->resource === $resource_id)
{
// Stop loop
$matches = TRUE;
}
else
{
// Find last occurence of '.' separator
$last_dot_pos = strrpos($resource_id, '.');
if ($last_dot_pos !== FALSE)
{
// This rule might match more generally, try the next level of specificity
$resource_id = substr($resource_id, 0, $last_dot_pos);
}
else
{
// We can't make this any more general as there are no more dots
// And we haven't managed to match the resource requested
return FALSE;
}
}
}
// Now we know this rule matches the resource, check any match condition
if ( ! is_null($this->condition) AND ! $resource->acl_conditions(Auth::instance()->get_user(), $this->condition))
{
// Condition wasn't met (or doesn't exist)
return FALSE;
}
// All looks rosy!
return TRUE;
}
/**
* Override create to remove less specific rules when creating a rule
*
* @return $this
*/
public function create()
{
// Delete all more specifc rules for this role
$delete = DB::delete($this->_table)
->where($this->_fields['role']->column, '=', $this->_changed['role']);
// If resource is '*' we don't need any more rules - we just delete every rule for this role
if ($this->resource !== '*')
{
// Need to restrict to roles with equal or more specific resource id
$delete->where_open()
->where($this->_fields['resource']->column, '=', $this->resource)
->or_where($this->_fields['resource']->column, 'LIKE', $this->resource.'.%')
->where_close();
}
if ( ! is_null($this->action))
{
// If this rule has an action, only remove other rules with the same action
$delete->where($this->_fields['action']->column, '=', $this->action);
}
if ( ! is_null($this->condition))
{
// If this rule has a condition, only remove other rules with the same condition
$delete->where($this->_fields['condition']->column, '=', $this->condition);
}
// Do the delete
$delete->execute($this->_db);
// Create new rule
parent::create();
}
/**
* Override Default model actions
*
* @param bool $return_current [optional]
* @return mixed
*/
public function acl_actions($return_current = FALSE)
{
if ($return_current)
{
// We don't know anything about what the user intends to do with us!
return NULL;
}
// Return default model actions
return array('grant', 'revoke');
}
} // End Model_AACL_Rule

101
classes/sprig/aacl.php Normal file
View File

@ -0,0 +1,101 @@
<?php defined('SYSPATH') or die ('No direct script access.');
/**
* Base class for access controlled Sprig Models
*
* @see http://github.com/banks/aacl
* @package AACL
* @uses Auth
* @uses Sprig
* @author Paul Banks
* @copyright (c) Paul Banks 2010
* @license MIT
*/
abstract class Sprig_AACL extends Sprig implements AACL_Resource
{
/**
* AACL_Resource::acl_id() implementation
*
* @return string
*/
public function acl_id()
{
// Create unique id from primary key if it is set
if (is_array($this->_primary_key))
{
$id = '';
foreach ($this->_primary_key as $name)
{
$id .= (string) $this->$name;
}
}
else
{
$id = (string) $this->{$this->_primary_key};
}
if ( ! empty($id))
{
$id = '.'.$id;
}
// Model namespace, model name, pk
return 'm:'.strtolower($this->_model).$id;
}
/**
* AACL_Resource::acl_actions() implementation
*
* @param bool $return_current [optional]
* @return mixed
*/
public function acl_actions($return_current = FALSE)
{
if ($return_current)
{
// We don't know anything about what the user intends to do with us!
return NULL;
}
// Return default model actions
return array('create', 'read', 'update', 'delete');
}
/**
* AACL_Resource::acl_conditions() implementation
*
* @param Model_User $user [optional] logged in user model
* @param object $condition [optional] condition to test
* @return mixed
*/
public function acl_conditions(Model_User $user = NULL, $condition = NULL)
{
if (is_null($user) AND is_null($condition))
{
// We have no conditions - they will be model specific
return array();
}
else
{
// We have no conditions so this test should fail!
return FALSE;
}
}
/**
* AACL_Resource::acl_instance() implementation
*
* Note that the object instance returned should not be used for anything except querying the acl_* methods
*
* @param string Class name of object required
* @return Object
*/
public static function acl_instance($class_name)
{
$model_name = strtolower(substr($class_name, 6));
return Sprig::factory($model_name);
}
} // End Sprig_AACL