1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Initial commit of ACL capabilities.

This commit is contained in:
Ben Alex
2004-07-29 07:51:22 +00:00
parent b749e83b1d
commit 56829872b6
35 changed files with 4084 additions and 3 deletions
+377 -3
View File
@@ -69,7 +69,7 @@
<sect2 id="security-high-level-design-key-components">
<title>Key Components</title>
<para>The Acegi Security System for Spring essentially comprises six
<para>The Acegi Security System for Spring essentially comprises seven
key functional parts:</para>
<itemizedlist spacing="compact">
@@ -109,6 +109,11 @@
authentication, authorization, run-as replacement and execution of
a given operation.</para>
</listitem>
<listitem>
<para>An acess control list (ACL) management package, which can be
used to obtain ACLs for domain object instances.</para>
</listitem>
</itemizedlist>
<para>Secure objects refer to any type of object that can have
@@ -134,8 +139,8 @@
<literal>FilterInterceptor</literal>) with complete
transparency.</para>
<para>Each of the six key parts is discussed in detail throughout this
document.</para>
<para>Each of the seven key parts is discussed in detail throughout
this document.</para>
</sect2>
<sect2 id="security-high-level-design-supported-secure-objects">
@@ -2985,6 +2990,370 @@ $CATALINA_HOME/bin/startup.sh</programlisting></para>
</sect2>
</sect1>
<sect1 id="acls">
<title>Instance-Based Access Control List (ACL) Security</title>
<sect2 id="acls-overview">
<title>Overview</title>
<para>THIS FEATURE WAS ADDED IN VERSION 0.6. WE WELCOME YOUR COMMENTS
AND IMPROVEMENTS.</para>
<para>Complex applications often will find the need to define access
permissions not simply at a web request or method invocation level.
Instead, security decisions need to comprise both who
(<literal>Authentication</literal>), where
(<literal>MethodInvocation</literal>) and what
(<literal>SomeDomainObject</literal>). In other words, authorization
decisions also need to consider the actual domain object instance
subject of a method invocation.</para>
<para>Imagine you're designing an application for a pet clinic. There
will be two main groups of users of your Spring-based application:
staff of the pet clinic, as well as the pet clinic's customers. The
staff will have access to all of the data, whilst your customers will
only be able to see their own customer records. To make it a little
more interesting, your customers can allow other users to see their
customer records, such as their "puppy preschool "mentor or president
of their local "Pony Club". Using Acegi Security System for Spring as
the foundation, you have several approaches that can be
used:<orderedlist>
<listitem>
<para>Write your business methods to enforce the security. You
could consult a collection within the
<literal>Customer</literal> domain object instance to determine
which users have access. By using the
<literal>ContextHolder.getContext()</literal> and casting it to
<literal>SecureContext</literal>, you'll be able to access the
<literal>Authentication</literal> object.</para>
</listitem>
<listitem>
<para>Write an <literal>AccessDecisionVoter</literal> to enforce
the security from the <literal>GrantedAuthority[]</literal>s
stored in the <literal>Authentication</literal> object. This
would mean your <literal>AuthenticationManager</literal> would
need to populate the <literal>Authentication</literal> with
custom <literal>GrantedAuthority</literal>[]s representing each
of the <literal>Customer</literal> domain object instances the
principal has access to.</para>
</listitem>
<listitem>
<para>Write an <literal>AccessDecisionVoter</literal> to enforce
the security and open the target <literal>Customer</literal>
domain object directly. This would mean your voter needs access
to a DAO that allows it to retrieve the
<literal>Customer</literal> object. It would then access the
<literal>Customer</literal> object's collection of approved
users and make the appropriate decision.</para>
</listitem>
</orderedlist></para>
<para>Each one of these approaches is perfectly legitimate. However,
the first couples your authorization checking to your business code.
The main problems with this include the enhanced difficulty of unit
testing and the fact it would be more difficult to reuse the
<literal>Customer</literal> authorization logic elsewhere. Obtaining
the <literal>GrantedAuthority[]</literal>s from the
<literal>Authentication</literal> object is also fine, but will not
scale to large numbers of <literal>Customer</literal>s. If a user
might be able to access 5,000 <literal>Customer</literal>s (unlikely
in this case, but imagine if it were a popular vet for a large Pony
Club!) the amount of memory consumed and time required to construct
the <literal>Authentication</literal> object would be undesirable. The
final method, opening the <literal>Customer</literal> directly from
external code, is probably the best of the three. It achieves
separation of concerns, and doesn't misuse memory or CPU cycles, but
it is still inefficient in that both the
<literal>AccessDecisionVoter</literal> and the eventual business
method itself will perform a call to the DAO responsible for
retrieving the <literal>Customer</literal> object. Two accesses per
method invocation is clearly undesirable. In addition, with every
approach listed you'll need to write your own access control list
(ACL) persistence and business logic from scratch.</para>
<para>Fortunately, there is another alternative, which we'll talk
about below.</para>
</sect2>
<sect2 id="acls-acl-package">
<title>The net.sf.acegisecurity.acl Package</title>
<para>The <literal>net.sf.acegisecurity.acl</literal> package is very
simple, comprising only a handful of interfaces and a single class. It
provides the basic foundation for access control list (ACL) lookups.
The central interface is <literal>AclManager</literal>, which is
defined by two methods:</para>
<para><programlisting>public AclEntry[] getAcls(java.lang.Object domainInstance);
public AclEntry[] getAcls(java.lang.Object domainInstance, Authentication authentication);</programlisting></para>
<para><literal>AclManager</literal> is intended to be used as a
collaborator against your business objects, or, more desirably,
<literal>AccessDecisionVoter</literal>s. This means you use Spring's
normal <literal>ApplicationContext</literal> features to wire up your
<literal>AccessDecisionVoter</literal> (or business method) with an
<literal>AclManager</literal>. Consideration was given to placing the
ACL information in the <literal>ContextHolder</literal>, but it was
felt this would be inefficient both in terms of memory usage as well
as the time spent loading potentially unused ACL information. The
trade-off of needing to wire up a collaborator for those objects
requiring ACL information is rather minor, particularly in a
Spring-managed application.</para>
<para>The first method of the <literal>AclManager</literal> will
return all ACLs applying to the domain object instance passed to it.
The second method does the same, but only returns those ACLs which
apply to the passed <literal>Authentication</literal> object.</para>
<para>The <literal>AclEntry</literal> interface returned by
<literal>AclManager</literal> is merely a marker interface. You will
need to provide an implementation that reflects that ACL permissions
for your application.</para>
<para>Rounding out the <literal>net.sf.acegisecurity.acl</literal>
package is an <literal>AclProviderManager</literal> class, with a
corresponding <literal>AclProvider</literal> interface.
<literal>AclProviderManager</literal> is a concrete implementation of
<literal>AclManager</literal>, which iterates through registered
<literal>AclProvider</literal>s. The first
<literal>AclProvider</literal> that indicates it can authoritatively
provide ACL information for the presented domain object instance will
be used. This is very similar to the
<literal>AuthenticationProvider</literal> interface used for
authentication.</para>
<para>With this background, let's now look at a usable ACL
implementation.</para>
</sect2>
<sect2 id="acls-masking">
<title>Integer Masked ACLs</title>
<para>Acegi Security System for Spring includes a production-quality
ACL provider implementation. The implementation is based on integer
masking, which is commonly used for ACL permissions given its
flexibility and speed. Anyone who has used Unix's
<literal>chmod</literal> command will know all about this type of
permission masking (eg <literal>chmod 777</literal>). You'll find the
classes and interfaces for the integer masking ACL package under
<literal>net.sf.acegisecurity.acl.basic</literal>.</para>
<para>Extending the <literal>AclEntry</literal> interface is a
<literal>BasicAclEntry</literal> interface, with the main methods
shown below:</para>
<para><programlisting>public AclObjectIdentity getAclObjectIdentity();
public AclObjectIdentity getAclObjectParentIdentity();
public int getMask();
public java.lang.Object getRecipient();</programlisting></para>
<para>As shown, each <literal>BasicAclEntry</literal> has four main
properties. The <literal>mask</literal> is the integer that represents
the permissions granted to the <literal>recipient</literal>. The
<literal>aclObjectIdentity</literal> is able to identify the domain
object instance for which the ACL applies, and the
<literal>aclObjectParentIdentity</literal> optionally specifies the
parent of the domain object instance. Multiple
<literal>BasicAclEntry</literal>s usually exist against a single
domain object instance, and as suggested by the parent identity
property, permissions granted higher in the object hierarchy will
trickle down and be inherited (unless blocked by integer zero).</para>
<para><literal>BasicAclEntry</literal> implementations typically
provide convenience methods, such as
<literal>isReadAllowed()</literal>, to avoid application classes
needing to perform bit masking themselves. The
<literal>SimpleAclEntry</literal> and
<literal>AbstractBasicAclEntry</literal> demonstrate and provide much
of this bit masking logic.</para>
<para>The <literal>AclObjectIdentity</literal> itself is merely a
marker interface, so you need to provide implementations for your
domain objects. However, the package does include a
<literal>NamedEntityObjectIdentity</literal> implementation which will
suit many needs. The <literal>NamedEntityObjectIdentity</literal>
identifies a given domain object instance by the classname of the
instance and the identity of the instance. A
<literal>NamedEntityObjectIdentity</literal> can be constructed
manually (by calling the constructor and providing the classname and
identity <literal>String</literal>s), or by passing in any domain
object that contains a <literal>getId()</literal> method.</para>
<para>The actual <literal>AclProvider</literal> implementation is
named <literal>BasicAclProvider</literal>. It has adopted a similar
design to that used by the authentication-related
<literal>DaoAuthenticationProvder</literal>. Specifically, you define
a <literal>BasicAclDao</literal> against the provider, so different
ACL repository types can be accessed in a pluggable manner. The
<literal>BasicAclProvider</literal> also supports pluggable cache
providers (with Acegi Security System for Spring including an
implementation that fronts EH-CACHE).</para>
<para>The <literal>BasicAclDao</literal> interface is very simple to
implement:</para>
<para><programlisting>public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity);</programlisting></para>
<para>A <literal>BasicAclDao</literal> implementation needs to
understand the presented <literal>AclObjectIdentity</literal> and how
it maps to a storage repository, find the relevant records, and create
appropriate <literal>BasicAclEntry</literal> objects and return
them.</para>
<para>Acegi Security includes a single <literal>BasicAclDao</literal>
implementation called <literal>JdbcDaoImpl</literal>. As implied by
the name, it accesses ACL information from a JDBC database. The
default database schema and some sample data will aid in understanding
its function:</para>
<para><programlisting>CREATE TABLE acls (
object_identity VARCHAR_IGNORECASE(250) NOT NULL,
recipient VARCHAR_IGNORECASE(100) NOT NULL,
parent_object_identity VARCHAR_IGNORECASE(250),
mask INTEGER NOT NULL,
acl_class VARCHAR_IGNORECASE(250) NOT NULL,
CONSTRAINT pk_acls PRIMARY KEY(object_identity, recipient)
);
INSERT INTO acls VALUES ('corp.DomainObject:1', 'ROLE_SUPERVISOR', null, 1, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:2', 'ROLE_SUPERVISOR', 'corp.DomainObject:1', 0, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:2', 'marissa', 'corp.DomainObject:1', 2, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:3', 'scott', 'corp.DomainObject:1', 14, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:4', 'inheritance_marker_only', 'corp.DomainObject:1', 0, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:5', 'inheritance_marker_only', 'corp.DomainObject:3', 0, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:6', 'scott', 'corp.DomainObject:3', 1, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
INSERT INTO acls VALUES ('corp.DomainObject:7', 'scott', 'some.invalid.parent:1', 2, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');</programlisting></para>
<para>The <literal>JdbcDaoImpl</literal> will only respond to requests
for <literal>NamedEntityObjectIdentity</literal>s. It converts such
identities into a single <literal>String</literal>, comprising
the<literal> NamedEntityObjectIdentity.getClassname()</literal> +
<literal>":"</literal> +
<literal>NamedEntityObjectIdentity.getId()</literal>. This yields the
type of <literal>object_identity</literal> values shown above. As
indicated by the sample data, each database row corresponds to a
single <literal>BasicAclEntry</literal>. As stated earlier and
demonstrated by <literal>corp.DomainObject:2</literal> in the above
sample data, each domain object instance will often have multiple
<literal>BasicAclEntry</literal>[]s.</para>
<para>As <literal>JdbcDaoImpl</literal> is required to return concrete
<literal>BasicAclEntry</literal> classes, it needs to know which
<literal>BasicAclEntry</literal> implementation it is to create and
populate. This is the role of the <literal>acl_class</literal> column.
<literal>JdbcDaoImpl</literal> will create the indicated class and set
its <literal>mask</literal>, <literal>recipient</literal>,
<literal>aclObjectIdentity</literal> and
<literal>aclObjectParentIdentity</literal> properties.</para>
<para>As you can probably tell from the sample data, the
<literal>parent_object_identity</literal> value can either be null or
in the same format as the <literal>object_identity</literal>. If
non-null, <literal>JdbcDaoImpl</literal> will create a
<literal>NamedEntityObjectIdentity</literal> to place inside the
returned <literal>BasicAclEntry</literal> class.</para>
<para>Returning to the <literal>BasicAclProvider</literal>, before it
can poll the <literal>BasicAclDao</literal> implementation it needs to
convert the domain object instance it was passed into an
<literal>AclObjectIdentity</literal>.
<literal>BasicAclProvider</literal> has a <literal>protected
AclObjectIdentity obtainIdentity(Object domainInstance)</literal>
method that is responsible for this. As a protected method, it enables
subclasses to easily override. The normal implementation checks
whether the passed domain object instance implements the
<literal>AclObjectIdentityAware</literal> interface, which is merely a
getter for an <literal>AclObjectIdentity</literal>. If the domain
object does implement this interface, that is the identity returned.
If the domain object does not implement this interface, the method
will attempt to create an <literal>AclObjectIdentity</literal> by
passing the domain object instance to the constructor of a class
defined by the
<literal>BasicAclProvider.getDefaultAclObjectIdentity()</literal>
method. By default the defined class is
<literal>NamedEntityObjectIdentity</literal>, which was described in
more detail above. Therefore, you will need to either (i) provide a
<literal>getId()</literal> method on your domain objects, (ii)
implement <literal>AclObjectIdentityAware</literal> on your domain
objects, (iii) provide an alternative
<literal>AclObjectIdentity</literal> implementation that will accept
your domain object in its constructor, or (iv) override the
<literal>obtainIdentity(Object)</literal> method.</para>
<para>Once the <literal>AclObjectIdentity</literal> of the domain
object instance is determined, the <literal>BasicAclProvider</literal>
will poll the DAO to obtain its <literal>BasicAclEntry</literal>[]s.
If any of the entries returned by the DAO indicate there is a parent,
that parent will be polled, and the process will repeat until there is
no further parent. The permissions assigned to a
<literal>recipient</literal> closest to the domain object instance
will always take priority and override any inherited permissions. From
the sample data above, the following inherited permissions would
apply:</para>
<para><programlisting>--- Mask integer 0 = no permissions
--- Mask integer 1 = administer
--- Mask integer 2 = read
--- Mask integer 6 = read and write permissions
--- Mask integer 14 = read and write and create permissions
---------------------------------------------------------------------
--- *** INHERITED RIGHTS FOR DIFFERENT INSTANCES AND RECIPIENTS ***
--- INSTANCE RECIPIENT PERMISSION(S) (COMMENT #INSTANCE)
---------------------------------------------------------------------
--- 1 ROLE_SUPERVISOR Administer
--- 2 ROLE_SUPERVISOR None (overrides parent #1)
--- marissa Read
--- 3 ROLE_SUPERVISOR Administer (from parent #1)
--- scott Read, Write, Create
--- 4 ROLE_SUPERVISOR Administer (from parent #1)
--- 5 ROLE_SUPERVISOR Administer (from parent #3)
--- scott Read, Write, Create (from parent #3)
--- 6 ROLE_SUPERVISOR Administer (from parent #3)
--- scott Administer (overrides parent #3)
--- 7 scott Read (invalid parent ignored)</programlisting></para>
<para>So the above explains how a domain object instance has its
<literal>AclObjectIdentity</literal> discovered, and the
<literal>BasicAclDao</literal> will be polled successively until an
array of inherited permissions is constructed for the domain object
instance. The final step is to determine the
<literal>BasicAclEntry</literal>[]s that are actually applicable to a
given <literal>Authentication</literal> object.</para>
<para>As you would recall, the <literal>AclManager</literal> (and all
delegates, up to and including <literal>BasicAclProvider</literal>)
provides a method which returns only those
<literal>BasicAclEntry</literal>[]s applying to a passed
<literal>Authentication</literal> object.
<literal>BasicAclProvider</literal> delivers this functionality by
delegating the filtering operation to an
<literal>EffectiveAclsResolver</literal> implementation. The default
implementation,
<literal>GrantedAuthorityEffectiveAclsResolver</literal>, will iterate
through the <literal>BasicAclEntry</literal>[]s and include only those
where the <literal>recipient</literal> is equal to either the
<literal>Authentication</literal>'s <literal>principal</literal> or
any of the <literal>Authentication</literal>'s
<literal>GrantedAuthority</literal>[]s. Please refer to the JavaDocs
for more information.</para>
</sect2>
<sect2 id="acls-conclusion">
<title>Conclusion</title>
<para>Acegi Security's instance-specific ACL packages shield you from
much of the complexity of developing your own ACL approach. The
interfaces and classes detailed above provide a scalable, customisable
ACL solution that is decoupled from your application code. Whilst the
reference documentation may suggest complexity, the basic
implementation is able to support most typical applications
out-of-the-box.</para>
</sect2>
</sect1>
<sect1 id="security-filters">
<title>Filters</title>
@@ -3081,6 +3450,11 @@ $CATALINA_HOME/bin/startup.sh</programlisting></para>
<para>All of the above filters use
<literal>FilterToBeanProxy</literal>, which is discussed in the
previous section.</para>
<para>If you're using SiteMesh, ensure the Acegi Security filters
execute before the SiteMesh filters are called. This enables the
<literal>ContextHolder</literal> to be populated in time for use by
SiteMesh decorators.</para>
</sect2>
</sect1>