#acl #access-control #privilege-management #access-control-lists

zorq-acl

Lightweigth and flexible access control list (ACL) implementation for privilege management

1 unstable release

0.1.0 Aug 10, 2020

#14 in #acl

MIT license

51KB
494 lines

zorq-acl

Lightweigth and flexible access control list (ACL) implementation for privilege management.

This is an adoption of the laminas-permissions-acl to Rust.

Getting Started

zorq-acl is available on crates.io. Look there for the newest released version, as well as links to the docs.

Add the following dependency to your Cargo manifest ...

[dependencies]
zorq-acl = "0.1.0"

lib.rs:

Lightweigth and flexible access control list (ACL) implementation for privilege management.

This is an adoption of the laminas-permissions-acl to Rust. The following documentation is a copy and adoption of the original documentation of "Laminas\Permissions\Acl\Acl". See file CREDITS for copyright notices on behalf of the Laminas project.

What is missing from the original implementation?

  • Removing access control. This will be implemented in a future version by a revoke method.
  • Ownership assertions and the role and resource interfaces. Ownership assertion may be implemented by traits defining the role and resource interface and by extending the api in the future.
  • Expression assertions. This may be implemented in a future version.

Introduction

In general an appilcation can utilize ACLs to allow or deny access to resources by requesting objects.

In the sense of this implementation:

  • a resource is an object to which access is controlled.
  • a role is an object that may request access to a resource.
  • a privilage is an action which may be granted on a resource to a role.

Resources

Resources are organized in a tree strcuture and must be named uniquely. Since resources are stored in such a tree structure, they can be oragnized from the general (tree root) to the specific (tree leafs). Queries on a specific resource will automatically search the resource's hierarchy for rules assigned to ancestor resources, allowing for simple inheritance of rules. For example, if a default rule is to be applied to each building in a city, one would simply assign the rule to the city, instead of assigning the same rule to each building. Some buildings may require exceptions to such a rule, however, by assigning such exception rules to each building that requires such an exception. A resource may inherit from only one parent resource, though this parent resource can have its own parent resource, etc.

Privileges on resources (e.g., "create", "read", "update", "delete") are also supported, so one can assign rules that affect all privileges or specific privileges on one or more resources.

Roles

A role may inherit from one or more roles. This is to support inheritance of rules among roles. For example, a user role, such as "sally", may belong to one or more parent roles, such as "editor" and "administrator". The developer can assign rules to "editor" and "administrator" separately, and "sally" would inherit such rules from both, without having to assign rules directly to "sally".

Though the ability to inherit from multiple roles is very useful, multiple inheritance also introduces some degree of complexity. The following example illustrates the ambiguity condition and how Acl solves it.

Multiple Inheritance among Roles

The following code defines three base roles - "guest", "member", and "admin" - from which other roles may inherit. Then, a role identified by "someUser" is established and inherits from the three other roles. The order in which these roles appear in the parents array is important. When necessary, searches for access include not only rules defined for the queried role (herein, "someUser"), but also upon the roles from which the queried role inherits (herein, "guest", "member", and "admin"):

acl.add_role("guest", vec![]);
acl.add_role("member", vec![]);
acl.add_role("admin", vec![]);

let parents = vec!["guest", "member", "admin"];

acl.add_role("someUser", parents);
acl.add_resource("someResource", None);

acl.deny(Some("guest"), Some("someResource"), None);
acl.allow(Some("member"), Some("someResource"), None);

assert!(acl.is_allowed(Some("someUser"), Some("someResource"), None));

Since there is no rule specifically defined for the "someUser" role and "someResource", the Acl must search for rules that may be defined for roles that "someUser" inherits. First, the "admin" role is visited, and there is no access rule defined for it. Next, the "member" role is visited, and the Acl finds that there is a rule specifying that "member" is allowed access to "someResource".

If the Acl were to continue examining the rules defined for other parent roles, however, it would find that "guest" is denied access to "someResource". This fact introduces an ambiguity because now "someUser" is both denied and allowed access to "someResource", by reason of having inherited conflicting rules from different parent roles.

The Acl resolves this ambiguity by completing a query when it finds the first rule that is directly applicable to the query. In this case, since the "member" role is examined before the "guest" role, the example code assertion is met and hence access is allowed.

LIFO Order for Role Queries: When specifying multiple parents for a role, keep in mind that the last parent listed is the first one searched for rules applicable to an authorization query.

Creating the Access Control List

An Access Control List (ACL) can represent any set of physical or virtual objects that you wish. For the purposes of demonstration, however, we will create a basic Content Management System (CMS) ACL that maintains several tiers of groups over a wide variety of areas. To create a new ACL object, we instantiate the ACL. The constructor has no parameters:

use zorq_acl::Acl;

let mut acl = Acl::new();

Denied by default

Until a developer specifies an "allow" rule, the Acl denies access to every privilege upon every resource by every role.

Registering Roles

CMS systems will nearly always require a hierarchy of permissions to determine the authoring capabilities of its users. There may be a 'Guest' group to allow limited access for demonstrations, a 'Staff' group for the majority of CMS users who perform most of the day-to-day operations, an 'Editor' group for those responsible for publishing, reviewing, archiving and deleting content, and finally an 'Administrator' group whose tasks may include all of those of the other groups as well as maintenance of sensitive information, user management, back-end configuration data, backup and export. This set of permissions can be represented in a role registry, allowing each group to inherit privileges from 'parent' groups, as well as providing distinct privileges for their unique group only. The permissions may be expressed as follows:

Name Unique Permissions Inherit Permissions From
Guest View N/A
Staff Edit, Submit, Revise Guest
Editor Publish, Archive, Delete Staff
Administrator (Granted all access) N/A

These groups can be added to the role registry as follows:

acl.add_role("guest", vec![]);
acl.add_role("staff", vec!["guest"]);
acl.add_role("editor", vec!["staff"]);
acl.add_role("admin", vec![]);

Defining Access Controls

Now that the ACL contains the relevant roles, rules can be established that define how resources may be accessed by roles. You may have noticed that we have not defined any particular resources for this example, which is simplified to illustrate that the rules apply to all resources. The Acl provides an implementation whereby rules need only be assigned from general to specific, minimizing the number of rules needed, because resources and roles inherit rules that are defined upon their ancestors.

Specificity: In general, the Acl obeys a given rule if and only if a more specific rule does not apply.

Consequently, we can define a reasonably complex set of rules with a minimum amount of code. To apply the base permissions as defined above:

// guest may only view content
acl.allow(Some("guest"), None, Some("view"));

// staff inherits view privilege from guest, but also needs additional privileges
acl.allow(Some("staff"), None, Some("edit"));
acl.allow(Some("staff"), None, Some("submit"));
acl.allow(Some("staff"), None, Some("revise"));

// editor inherits view, edit, submit, and revise privileges from staff, but also needs
// additional privileges
acl.allow(Some("editor"), None, Some("publish"));
acl.allow(Some("editor"), None, Some("archive"));
acl.allow(Some("editor"), None, Some("delete"));

// admin inherits nothing, but is allowed all privileges
acl.allow(Some("admin"), None, None);

The None values in the above allow() calls are used to indicate that the allow rules apply to all resources. None is equal to a wildcard.

Querying an ACL

We now have a flexible ACL that can be used to determine whether requesters have permission to perform functions throughout the web application. Performing queries is quite simple using the is_allowed or is_denied method:

// allowed
assert!( acl.is_allowed(Some("guest"), None, Some("view")));
assert!(!acl.is_denied (Some("guest"), None, Some("view")));

// denied
assert!(!acl.is_allowed(Some("staff"), None, Some("publish")));
assert!( acl.is_denied (Some("staff"), None, Some("publish")));

// allowed
assert!( acl.is_allowed(Some("staff"), None, Some("revise")));
assert!(!acl.is_denied (Some("staff"), None, Some("revise")));

// allowed because of inheritance from guest
assert!( acl.is_allowed(Some("editor"), None, Some("view")));
assert!(!acl.is_denied (Some("editor"), None, Some("view")));

// denied because no allow rule for 'update'
assert!(!acl.is_allowed(Some("editor"), None, Some("update")));
assert!( acl.is_denied (Some("editor"), None, Some("update")));

// allowed because admin is allowed all privileges
assert!( acl.is_allowed(Some("admin"), None, Some("view")));
assert!(!acl.is_denied (Some("admin"), None, Some("view")));

// allowed because admin is allowed all privileges
assert!( acl.is_allowed(Some("admin"), None, None));
assert!(!acl.is_denied (Some("admin"), None, None));

// allowed because admin is allowed all privileges
assert!( acl.is_allowed(Some("admin"), None, Some("update")));
assert!(!acl.is_denied (Some("admin"), None, Some("update")));

Precise Access Controls

The basic ACL as defined in the previous section shows how various privileges may be allowed upon the entire ACL (all resources). In practice, however, access controls tend to have exceptions and varying degrees of complexity. The Acl allows you to accomplish these refinements in a straightforward and flexible manner.

For the example CMS, it has been determined that whilst the 'staff' group covers the needs of the vast majority of users, there is a need for a new 'marketing' group that requires access to the newsletter and latest news in the CMS. The group is fairly self-sufficient and will have the ability to publish and archive both newsletters and the latest news.

In addition, it has also been requested that the 'staff' group be allowed to view news stories but not to revise the latest news. Finally, it should be impossible for anyone (administrators included) to archive any 'announcement' news stories since they only have a lifespan of 1-2 days.

First we revise the role registry to reflect these changes. We have determined that the 'marketing' group has the same basic permissions as 'staff', so we define 'marketing' in such a way that it inherits permissions from 'staff':

acl.add_role("marketing", vec!["staff"]);

Next, note that the above access controls refer to specific resources (e.g., "newsletter", "latest news", "announcement news"). Now we add these resources:

acl.add_resource("newsletter", None);
acl.add_resource("news", None);
acl.add_resource("latest", Some("news"));
acl.add_resource("anouncement", Some("news"));

Then it is simply a matter of defining these more specific rules on the target areas of the ACL:

// marketing must be able to publish and archive newsletters and the latest news
acl.allow(Some("marketing"), Some("newsletter"), Some("publish"));
acl.allow(Some("marketing"), Some("newsletter"), Some("archive"));
acl.allow(Some("marketing"), Some("latest"), Some("publish"));
acl.allow(Some("marketing"), Some("latest"), Some("archive"));

// staff (and marketing, by inheritance), are denied permission
// to revise the latest news
acl.deny(Some("staff"), Some("latest"), Some("revise"));

// everyone (including admins) are denied permission to archive news announcements
acl.deny(None, Some("anouncement"), Some("archive"));

We can now query the ACL with respect to the latest changes:

// denied
assert!(!acl.is_allowed(Some("staff"), Some("newsletter"), Some("publish")));
assert!( acl.is_denied (Some("staff"), Some("newsletter"), Some("publish")));

// allowed
assert!( acl.is_allowed(Some("marketing"), Some("newsletter"), Some("publish")));
assert!(!acl.is_denied (Some("marketing"), Some("newsletter"), Some("publish")));

// denied
assert!(!acl.is_allowed(Some("staff"), Some("latest"), Some("publish")));
assert!( acl.is_denied (Some("staff"), Some("latest"), Some("publish")));

// allowed
assert!( acl.is_allowed(Some("marketing"), Some("latest"), Some("publish")));
assert!(!acl.is_denied (Some("marketing"), Some("latest"), Some("publish")));

// allowed
assert!( acl.is_allowed(Some("marketing"), Some("latest"), Some("archive")));
assert!(!acl.is_denied (Some("marketing"), Some("latest"), Some("archive")));

// denied
assert!(!acl.is_allowed(Some("marketing"), Some("latest"), Some("revise")));
assert!( acl.is_denied (Some("marketing"), Some("latest"), Some("revise")));

// denied
assert!(!acl.is_allowed(Some("editor"), Some("anouncement"), Some("archive")));
assert!( acl.is_denied (Some("editor"), Some("anouncement"), Some("archive")));

// denied
assert!(!acl.is_allowed(Some("admin"), Some("anouncement"), Some("archive")));
assert!( acl.is_denied (Some("admin"), Some("anouncement"), Some("archive")));

Dependencies

~87KB