Book Home Java Security Search this book

5.2. Permissions

The basic entity that the access controller operates on is a permission object--an instance of the Permission class (java.security.Permission). The Permission class itself is an abstract class that represents a particular operation. The nomenclature here is a little misleading, because a permission object can reflect two things. When it is associated with a class (through a code source and a protection domain), a permission object represents an actual permission that has been granted to that class. Otherwise, a permission object allows us to ask if we have a specific permission.

For example, if we construct a permission object that represents access to a file, possession of that object does not mean that we have permission to access the file. Rather, possession of the object allows us to ask if we have permission to access the file.

An instance of the Permission class represents one specific permission. A set of permissions--e.g., all the permissions that are given to classes signed by a particular individual--is represented by an instance of the Permissions class (java.security.Permissions). As developers and administrators, we'll make extensive use of these classes, so we'll need to investigate them in depth.

5.2.1. The Permission Class

Permissions have three properties:

A type

All permissions carry a basic type that identifies what the permission pertains to. A permission object to access a file will have a type of FilePermission; an object to create a window will have a type of AWTPermission; permission to use the XYZ company payroll application would have a type of XYZPayrollPermission.

A name

All permissions have a name that identifies the specific object that a permission relates to. A FilePermission has a name that is the name of the file to be accessed; an AWTPermission to create a window has a name of showWindowWithoutWarningBanner; permission to access a particular employee's payroll record would have the name of that employee. Names are often based on wildcards, so that a single file permission object may represent permission to access several files, and so on.

The name of a permission is fairly arbitrary. In the case of file permissions, the name is obviously the file. But the name of the showWindowWithoutWarningBanner permission (among many others) is chosen by convention, and it is up to all Java programs to adhere to that convention. This is only a concern to programmers when dealing with your own permission classes; as a developer you rarely need to create permission objects for the types of permissions defined in the Java API.

On the other hand, this naming convention is of concern to end users and administrators, who must know the name of the permission they want to grant to the programs they are going to run. These names must go into the policy file (which we'll discuss in just a bit).

Actions

Some permissions carry with them one or more actions. The presence of these actions is dependent upon the semantics of the specific type of permission. A file permission object has a list of actions that could include read, write, and delete; an XYZ payroll permission object could have a list of actions that includes view and update. On the other hand, a window permission does not have an action: you either have permission to create the window, or you don't. Actions can also be specified by wildcards.The terms used to specify a list of actions are also arbitrary and handled by convention.

Permissions can serve two roles. They allow the Java API to negotiate access to several resources (files, sockets, and so on). Those permissions are defined by convention within the Java API, and their naming conventions are wholly within the domain of the Java API itself. Hence, you can create an object that represents permission to read a particular file, but you cannot create an object that represents permission to copy a particular file, since the copy action is not known within the file permission class.

On the other hand, you can create arbitrary permissions for use within your own programs and completely define both the names of those permissions as well as the actions (if any) that should apply. If you are writing a payroll program, for example, you could create your own permission class that uses the convention that the name of the permission is the employee upon whose payroll information you want to act; you could use the convention that the permissible actions on the payroll permission are view and update. Then you can use that permission in conjunction with the access controller to allow employees to view their own payroll data and to allow managers to change the payroll data for their employees.

We'll look at both of these cases, starting with the classes that are provided within the Java API itself. These classes are used by the Java API (and in particular, by the security manager) to protect access to certain resources in ways that are fairly intuitive, given our knowledge of the security manager (but we'll examine that interaction in detail later).

5.2.2. Permissions of the Java API

There are 11 standard permissions in the Java API, each of which is implemented as a class:

  1. The FilePermission class (java.io.FilePermission)

    This class represents permissions for files. This class implements two wildcard patterns for filenames: an asterisk matches all files in a given directory, and a hyphen matches all files that reside in an entire directory hierarchy. Valid actions for file permissions are read, write, delete, and execute.

    File permissions must be constructed with their platform-specific name. Hence, /myclasses/xyz is a valid name for a file permission on a Unix system, but not on a Macintosh (where an equivalent name might be System Disk:myclasses:xyz). When these strings are specified programmatically, they are not too difficult to construct (using the file separator property); when these strings need to be specified in an external file, an appropriate syntax must be used.

    Keep in mind the difference between an asterisk and a hyphen: an asterisk only traverses a single directory, while a hyphen traverses an entire filesystem. Hence /myclasses/* will not include /myclasses/xyz/HRApplet.class, but /my-classes/- will. A single asterisk will access all files in the current directory, and a single hyphen will access all files in the current directory and its subdirectories.

    If you want to access all files on a particular machine, you specify the special token <<ALL FILES>>.

    A FilePermission object is constructed by providing the name of the file and a list of actions on that file:

    Class Definition

    FilePermission p1 = new FilePermission("-", "execute");
    FilePermission p2 = new FilePermission("/myclasses/*", "read, write");
    FilePermission p3 = new FilePermission("<<ALL FILES>>", "read");

    Here, p1 represents permission to execute all files that are in the filesystem hierarchy under the current directory, p2 represents permission to read and write all files that exist in the directory /myclasses, and p3 represents permission to read all the files on the machine.

  2. The SocketPermission class (java.net.SocketPermission)

    This class represents permissions to interact with network sockets. The name of a socket permission is hostname:port, where each component of the name may be specified by a wildcard. In particular, the hostname may be given as a hostname (possibly DNS qualified) or an IP address. The leftmost position of the hostname may be specified as an asterisk, such that the host piccolo.East.Sun.COM would be matched by each of these strings:

    Class Definition

    piccolo
    piccolo.East.Sun.COM
    *.Sun.COM
    *
    129.151.119.8

    The port component of the name can be specified as a single port number or as a range of port numbers (e.g., 1-1024). When a range is specified, either side of the range may be excluded:

    Class Definition

    1024 (port 1024)
    1024- (all ports greater than or equal to 1024)
    -1024 (all ports less than or equal to 1024)
    1-1024 (all ports between 1 and 1024, inclusive)

    Valid actions for a socket permission are accept, connect, listen, and resolve. These map into the socket API: accept is used by the ServerSocket class to see if it can accept an incoming connection from a particular host; connect is used by the Socket class to see if it can make a connection to a particular host, listen is used by the ServerSocket class to see if a server socket can be created at all, and resolve is used by the Socket class to see if the IP address for a particular host can be obtained.

    Constructing a socket permission, then, is simply a matter of putting together the desired strings in the correct format:

    Class Definition

    SocketPermission s1 = new SocketPermission("piccolo:6000", "connect");
    SocketPermission s2 = new SocketPermission("piccolo:1024-",
    					 "accept, listen");

    Here s1 represents permission to connect to the X server (port 6000) on machine piccolo, and s2 represents permission for piccolo to start a server on any nonprivileged port.

  3. The PropertyPermission class (java.util.PropertyPermission)

    This class represents permissions for Java properties. Property permission names are specified as dot-separated names (just as they are in a Java property file); in addition, the last element can be a wildcard asterisk: *, a.*, a.b.*, and so on.

    The valid actions for this class are read and write. Hence, to construct a property permission, you would do something like:

    Class Definition

    PropertyPermission p1 = new PropertyPermission("java.version", "read");
    PropertyPermission p2 = new PropertyPermission("xyz.*", "read,write");

    Here, p1 represents permission to read the version of the virtual machine that's in use, and p2 represents permission to read or write all properties that begin with the token xyz.

  4. The RuntimePermission class (java.lang.RuntimePermission)

    This class represents permissions for the Java runtime--essentially, permissions to perform any of the operations encapsulated by the Runtime class, including most thread operations. The names recognized by this class are dot-separated names and are subject to the same wildcard asterisk matching as the property permission class.

    Runtime permissions have no associated actions--you either have permission to perform those operations, or you don't. Hence, a runtime permission is constructed as:

    Class Definition

    RuntimePermission r1 = new RuntimePermission("exitVM");
    RuntimePermission r2 = new 
    	RuntimePermission("accessClassInPackage.java");

    Here, r1 represents permission to exit the virtual machine, and r2 represents permission to access classes in the java package.

  5. The AWTPermission class (java.awt.AWTPermission)

    This class represents permissions to access certain windowing resources. In particular, as we might assume from the corresponding methods in the security manager, there are three conventional names in this class: showWindowWithoutWarningBanner, accessClipboard, and accessEventQueue.

    There are no actions associated with this class. In addition, this class technically supports wildcard matching, but since none of the conventional names are in dot-separated format, that facility is unused. Hence, an AWT permission is constructed like this:

    Class Definition

    AWTPermission a = new AWTPermission("showWindowWithoutWarningBanner");
  6. The NetPermission class (java.net.NetPermission)

  7. The NetPermission class (java.net.NetPermission)

    This class represents permissions to interact with two different classes. The first is the Authenticator class: there are no concrete implementations of the Authenticator class within the JDK, but implementations of that class provide HTTP authentication for password-protected web pages. The valid names associated with this class are setDefaultAuthenticator and requestPasswordAuthentication. Wildcard asterisk matching applies to these names.

    In addition, this class encapsulates various URL-related permissions. Permission to specify a stream handler in the URL class is named specifyStreamHandler.

    There are no associated actions with a net permission, so they are constructed as follows:

    Class Definition

    NetPermission n1 = new NetPermission("requestPasswordAuthentication");
  8. The SecurityPermission class (java.security.SecurityPermission)

    This class represents permission to use the security package. Names passed to this class are subject to wildcard asterisk matching, and there are no actions associated with this class. The valid names to this class include all the valid strings that can be passed to the checkSecurityAccess() method of the security manager; as we discuss the security API in the last half of this book, we'll list these names for each class.

  9. TheSerializablePermission class (java.io.SerializablePermission)

    This class represents various permissions relating to the serialization and deserialization of an object. No wildcards or actions are accepted by this class. This permission has two valid names: enableSubstitution and enableSubclassImplementation. The first of these permissions allows the enableResolveObject() method of the ObjectInputStream and the enableReplaceObject() method of the ObjectOutputStream classes to function. The latter permission allows the ObjectInputStream and ObjectOutputStream classes to be subclassed, which would potentially override the readObject() and writeObject() methods.

  10. The ReflectPermission class (java.lang.reflect.ReflectPermission)

    This permission represents the ability to set the accessible flag on objects that are to be used with the reflection API. This class has a single name (suppressAccessChecks) and no actions.

  11. The UnresolvedPermission class (java.security.UnresolvedPermission)

    This class is used internally in the Java API to represent external permissions (i.e., permissions that are implemented by third-party APIs) before the class that defines that permission is found. This permission is only needed if you are writing an implementation of the Policy class.

  12. The AllPermission class (java.security.AllPermission)

    This class represents permission to perform any operation--including file, socket, and other operations that have their own permission classes. Granting this type of permission is obviously somewhat dangerous; this permission is usually given only to classes within the Java API and to classes in Java extensions. This class has no name or actions; it is constructed as follows:

    Class Definition

    AllPermission ap = new AllPermission();

5.2.3. Using the Permission Class

We'll now look into the classes upon which all these permissions are based: the Permission class. This class abstracts the notion of a permission and a name. From a programmatic standpoint, the Permission class is really used only to create your own types of permissions. It has some interesting methods, but the operations that are implemented on a permission object are not generally used in code that we write--they are used instead by the access controller. Hence, we'll examine this class primarily with an eye towards understanding how it can be used to implement our own permissions.

Permission is an abstract class that contains these public methods:

public Permission(String name) figure

Construct a permission object that represents the desired permission.

public abstract boolean equals(Object o) figure

Subclasses of the Permission class are required to implement their own test for equality. Often this is simply done by comparing the name (and actions, if applicable) of the permission.

public abstract int hashCode() figure

Subclasses of the Permission class are required to implement their own hash code. In order for the access controller to function correctly, the hash code for a given permission object must never change during execution of the virtual machine. In addition, permissions that compare as equal must return the same hash code from this method.

public final String getName() figure

Return the name that was used to construct this permission.

public abstract String getActions() figure

Return the canonical form of the actions (if any) that were used to construct this permission.

public String toString() figure

The convention for printing a permission is to print in parentheses the class name, the name of the permission, and the actions. For example, a file permission might return:

Class Definition

("java.io.FilePermission","/myclasses/xyz/HRApplet.class","read")
public abstract boolean implies(Permission p) figure

This method is one of the keys of the Permission class: it is responsible for determining whether or not a class that is granted one permission is granted another. This method is normally responsible for performing wildcard matching, so that, for example, the file permission /myclasses/- implies the file permission /myclasses/xyz/HRApplet.class. But this method need not rely on wildcards; permission to write a particular object in a database would probably imply permission to read that object as well.

public PermissionCollection newPermissionCollection() figure

Return a permission collection suitable for holding instances of this type of permission. We'll discuss the topic of permission collections in the next section. This method returns null by default.

public void checkGuard(Object o) figure

Call the security manager to see if the permission (i.e., the this variable) has been granted, generating a SecurityException if the permission has not been granted. The object parameter of this method is unused. We'll give more details about this method later in this chapter.

Implementing your own permission means providing a class with concrete implementations of these abstract methods. Note that the notions of wildcard matching and actions are not generally present in this class--if you want your class to support either of these features, you're responsible for implementing all of the necessary logic to do so (although the BasicPermission class that we'll look at next can help us with that).

Say that you are implementing a program to administer payroll information. You'll want to create permissions to allow users to view their payment history. You'll also want to allow the HR department to update the pay rate for employees. So we'll need to implement a permission class to encapsulate all of that:

Class Definition

public class XYZPayrollPermission extends Permission {

	protected int mask;
	static private int VIEW = 0x01;
	static private int UPDATE = 0x02;

	public XYZPayrollPermission(String name) {
		this(name, "view");
	}

	public XYZPayrollPermission(String name, String action) {
		super(name);
		parse(action);
	}

	private void parse(String action) {
		StringTokenizer st = new StringTokenizer(action, ",\t ");

		mask = 0;
		while (st.hasMoreTokens()) {
			String tok = st.nextToken();
			if (tok.equals("view"))
				mask |= VIEW;
			else if (tok.equals("update"))
				mask |= UPDATE;
			else throw new IllegalArgumentException(
									"Unknown action " + tok);
		}
	}

	public boolean implies(Permission permission) {
		if (!(permission instanceof XYZPayrollPermission))
			return false;
		
		XYZPayrollPermission p = (XYZPayrollPermission) permission;
		String name = getName();
		if (!name.equals("*") && !name.equals(p.getName()))
			return false;
		if ((mask & p.mask) != p.mask)
			return false;
		return true;
	}

	public boolean equals(Object o) {
		if (!(o instanceof XYZPayrollPermission))
			return false;
		
		XYZPayrollPermission p = (XYZPayrollPermission) o;
		return ((p.getName().equals(getName())) && (p.mask == mask));
	}

	public int hashCode() {
		return getName().hashCode() ^ mask;
	}

	public String getActions() {
		if (mask == 0)
			return "";
		else if (mask == VIEW)
			return "view";
		else if (mask == UPDATE)
			return "update";
		else if (mask == (VIEW | UPDATE))
			return "view, update";
		else throw new IllegalArgumentException("Unknown mask");
	}

	public PermissionCollection newPermissionsCollection() {
		return new XYZPayrollPermissionCollection();
	}
}

The instance variables in this class are required to hold the information about the actions--even though our superclass makes references to actions, it doesn't provide a manner in which to store them or process them, so we have to provide that logic. That logic is provided in the parse() method; we've chosen the common convention of having the action string treated as a list of actions that are separated by commas and whitespace. Note also that we've stored the actual actions as bits in a single integer--this simplifies some of the later logic.

As required, we've implemented the equals() and hashCode() methods--and we've done so rather simply. We consider objects equal if their names are equal and their masks (that is, their actions) are equal, and construct a hash code accordingly.

Our implementation of the getActions() method is typical: we're required to return the same action string for a permission object that was constructed with an action list of "view, update" as for one that was constructed with an action list of "update,view". This requirement is one of the prime reasons why the actions are stored as a mask--because it allows us to construct this action string in the proper format.

Finally, the implies() method is responsible for determining how wildcard and other implied permissions are handled. If the name passed to construct our object is an asterisk, then we match any other name; hence, an object to represent the permissions of the HR department might be constructed as:

Class Definition

new XYZPayrollPermission("*", "view, update")

When the implies() method is called on this wildcard object, the name will always match, and because the action mask has the complete list of actions, the mask comparison will always yield the mask that we're testing against. If the implies() method is called with a different object, however, it will only return true if the names are equal and the object's mask is a subset of the target mask.

Note that we also might have implemented the logic in such a way that permission to perform an update implies permission to perform a view simply by changing the logic of testing the mask--you're not limited only to wildcard matching in the implies() method.

5.2.4. The BasicPermission Class

If you need to implement your own permission class, the BasicPermission class (java.security.BasicPermission) provides some useful semantics. This class implements a basic permission--that is, a permission that doesn't have actions. Basic permissions can be thought of as binary permission--you either have them, or you don't. However, this restriction does not prevent you from implementing actions in your subclasses of the BasicPermission class (as the PropertyPermission class does).

The prime benefit of this class is the manner in which it implements wildcards. Names in basic permissions are considered to be hierarchical, following a dot-separated convention. For example, if the XYZ corporation wanted to create a set of basic permissions, they might use the convention that the first word of the permission always be xyz: xyz.readDatabase, xyz.writeDatabase, xyz.runPayrollProgram, xyz.HRDepartment.accessCheck, and so on. These permissions can then be specified by their full name, or they can be specified with an asterisk wildcard: xyz.* would match each of these (no matter what depth), and * would match every possible basic permission.

The wildcard matching of this class does not match partial names: xyz.read* would not match any of the permissions we just listed. Further, the wildcard must be in the rightmost position: *.readDatabase would not match any basic permission.

The BasicPermission class is abstract, although it does not contain any abstract methods, and it completely implements all the abstract methods of the Permission class. Hence, a concrete implementation of the BasicPermission need only contain a constructor to call the correct constructor of the superclass (since there is no default constructor in the BasicPermission class). Subclasses must call one of these constructors:

public BasicPermission(String name) figure

Construct a permission with the given name. This is the usual constructor for this class, as basic permissions do not normally have actions.

public BasicPermission(String name, String action) figure

Construct a permission with the given name and action. Even though basic permissions do not usually have actions associated with them, you must provide a constructor with this signature in all implementations of the BasicPermission class due to the mechanism that is used to construct permission objects from the policy file (which we will see later in this chapter).

5.2.5. Permission Collections

The access controller depends upon the ability to aggregate permissions so that it can easily call the implies() method on all of them. For example, a particular user might be given permission to read several directories: perhaps the user's home directory (/home/sdo/-) and the system's temporary directory (/tmp/-). When the access controller needs to see if the user can access a particular file, it must test both of these permissions to see if either one matches. This can be done easily by aggregating all the file permissions into a single permission collection.

Every permission class is required to implement a permission collection, then, which is a mechanism where objects of the same permission class may be grouped together and operated upon as a single unit. This requirement is enforced by the newPermissionCollection() method of the Permission class.

The PermissionCollection class (java.security.PermissionCollection) is defined as follows:

public abstract class PermissionCollection

Implement an aggregate set of permissions. While permission collections can handle heterogeneous sets of permissions, a permission collection typically should be used to group together a homogeneous group of permissions (e.g., all file permissions or all socket permissions, etc.).

There are three basic operations that you can perform on a permission collection:

public abstract void add(Permission p) figure

Add the given permission to the permission collection.

public abstract boolean implies(Permission p) figure

Check to see if any permission in the collection implies the given permission. This can be done by enumerating all the permission objects that have been added to the collection and calling the implies() method on each of those objects in turn, but it is typically implemented in a more efficient manner.

public abstract Enumeration elements() figure

Return an enumeration of all the permissions in the collection.

The javadoc documentation of this class claims that a permission collection is a collection of heterogeneous permission objects. Forget that idea; introducing that notion into permission collections vastly complicates matters, and the issue of a heterogeneous collection of permission objects is better handled elsewhere (we'll see how a little bit later). As far as we're concerned, the purpose of a permission collection is to aggregate only permission objects of a particular type.

Permission collections are typically implemented as inner classes, or at least as classes that are private to the package in which they are defined. There is, for example, a corresponding permission collection class for the FilePermission class, one for the SocketPermission class, and so on.

None of these collections is available as a public class that we can use in our own program. Hence, in order to support the newPermissionCollection() method in our XYZPayrollPermission class, we'd need to do something like this:

Class Definition

public class XYZPayrollPermissionCollection extends 
										PermissionCollection {
	private Hashtable permissions;
	private boolean addedAdmin;
	private int adminMask;

	XYZPayrollPermissionCollection() {
		permissions = new Hashtable();
		addedAdmin = false;
	}

	public void add(Permission p) {
		if (!(p instanceof XYZPayrollPermission))
			throw new IllegalArgumentException(
									"Wrong permission type");
		XYZPayrollPermission xyz = (XYZPayrollPermission) p;
		String name = xyz.getName();
		XYZPayrollPermission other =
						(XYZPayrollPermission) permissions.get(name);
		if (other != null)
			xyz = merge(xyz, other);
		if (name.equals("*")) {
			addedAdmin = true;
			adminMask = xyz.mask;
		}
		permissions.put(name, xyz);
	}

	public Enumeration elements() {
		return permissions.elements();
	}

	public boolean implies(Permission p) {
		if (!(p instanceof XYZPayrollPermission))
			return false;
		XYZPayrollPermission xyz = (XYZPayrollPermission) p;
		if (addedAdmin && (adminMask & xyz.mask) == xyz.mask)
			return true;
		Permission inTable = (Permission)
								permissions.get(xyz.getName());
		if (inTable == null)
			return false;
		return inTable.implies(xyz);
	}

	private XYZPayrollPermission
				merge(XYZPayrollPermission a, XYZPayrollPermission b) {
		String aAction = a.getActions();
		if (aAction.equals(""))
			return b;
		String bAction = b.getActions();
		if (bAction.equals(""))
			return a;
		return new XYZPayrollPermission(a.getName(),
									aAction + "," + bAction);
	}
}

Note the logic within the implies() method--it's the important part of this example. The implies() method must test each permission in the hashtable (or whatever other container you've used to store the added permissions), but it should do so efficiently. We could always call the implies() method of each entry in the hashtable, but that would clearly not be efficient--it's better to call only the implies() method on a permission in the table that has a matching name.

The only trick is that we won't find a matching name if we're doing wildcard pattern matching--if we've added the name "*" to the table, we'll always want to return true, even though looking up the name "John Smith" in the table will not return the administrative entry. Implementing this wildcard pattern matching efficiently is the key to writing a good permission collection.

When you use (or subclass) one of the concrete permission classes that we listed earlier, there is no need to provide a permission collection class--all concrete implementations provide their own collection. In addition, there are two other cases when you do not need to implement a permission collection:

If you implement your own PermissionCollection class, you must keep track of whether it has been marked as read-only. There are two methods invlolved in this:

public boolean isReadOnly() figure

Return an indication of whether the collection has been marked as read-only.

public void setReadOnly() figure

Set the collection to be read-only. Once the read-only flag has been set, it cannot be unset: the collection will remain read-only forever.

A permission collection is expected to throw a security exception from its add() method if it has been marked as read-only. Note that the read-only instance variable is private to the PermissionCollection class, so subclasses will have to rely on the isReadOnly() method to test its value.

5.2.6. The Permissions Class

So far, we've spoken about permission collections as homogeneous collections: all permissions in the XYZPayrollPermissionCollection class are instances of the XYZPayrollPermission class; a similar property holds for other permission collections. This idea simplifies the implies() method that we showed above. But to be truly useful, a permission collection needs to be heterogeneous, so it can represent all the permissions a program should have. A permission collection really needs to be able to contain file permissions, socket permissions, and other types of permissions.

This idea is present within the PermissionCollection class; conceptually, however, it is best to think of heterogeneous collections of permissions as encapsulated by the Permissions class (java.security.Permissions):

public final class Permissions extends PermissionCollection

Implement the PermissionCollection class. This class allows you to create a heterogeneous collection of permissions: the permission objects that are added to this collection need not have the same type.

This class contains a concrete implementation of a permission collection that organizes the aggregated permissions in terms of their individual, homogenous permission collections. You can think of a permissions object as containing an aggregation of permission collections, each of which contains an aggregation of individual permissions.

For example, let's consider an empty permissions object. When a file permission is added to this object, the permissions object will call the newPermissionCollection() method on the file permission to get a homogeneous file permission collection object. The file permission is then stored within this file permission collection. When another file permission is added to the permissions object, the permissions object will place that file permission into the already existing file permission collection object. When a payroll permission object is added to the permissions object, a new payroll permission collection will be obtained, the payroll permission added to it, and the collection added to the permissions object. This process will continue, and the permissions object will build up a set of permission collections.

When the implies() method of the permissions object is called, it will search its set of permission collections for a collection that can hold the given permission. It can then call the implies() method on that (homogenous) collection to obtain the correct answer.

The Permissions class thus supports any arbitrary grouping of permissions. There is no need to develop your own permission collection to handle heterogeneous groups.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.