Security

Overview

Security in the Niagara framework covers a couple of broad topics:

The following steps are used to setup a Niagara security model:

  1. First we have to define the users, which are modeled as BUsers.
  2. We have to authenticate users, to make sure they are who they say they are. This is done via a login, with a username and password or other credentials appropriate to the user's authentication scheme.
  3. We have to determine what each user can do with each object. The objects we typically wish to protect are Components, Files, and Histories. Each of these objects is categorized into one or more categories.
  4. We grant each user a set of permissions in each category. This defines exactly what each user can do with each object in the system.
  5. Last we audit anything a user does for later analysis.

Users

The BUser component models security principles in a Niagara system. Typically, BUsers map to human users, but can also be used to represent machine accounts for machine to machine logins.

The BUserService is used to store and lookup BUsers during login. The BUserService simply stores the system users as dynamic slots.

BUser is used to store the authentication credentials, roles, as well as any other required meta-data for each user. As a developer, if you wish to add additional meta-data to users, then you might consider declaring your own BIMixIn.

Authentication

For a detailed look at the BAuthenticationService, and how to create new BAuthenticationSchemes, see the authentication documentation.

All authentication in the Niagara framework is based on the BUserService and the BAuthenticationService configured for a station database.

The BAuthenticationService determines what BAuthenticationSchemes a station supports. These BAuthenticationSchemes are then assigned to each user in the BUserService

The BUserService is used to lookup BUsers by username during login, to determine what BAuthenticationScheme to use. This determines what types of credentials to acquire from the user, and how to acquire them. The credentials are then compared to the credentials stored in the BUserService.

There are three primary authentication points in the Niagara system:

  1. Fox Workbench to Station: When a connection is made from workbench to a station, the user is prompted for a username and credentials which are used to authenticate the Fox connection.
  2. Fox Station to Station: When a connection is made from a station to another station, preconfigured credentials are used to authenticate the Fox connection. These credentials are stored in the NiagaraStation.clientConnection component.
  3. HTTP Browser to Station: When a browser hits a station URL, an HTTP authentication mechanism is used to validate the user.

Authentication and Communication Protocols

Niagara 4 supports authentication over fox and over HTTP. However, not all BAuthenticationSchemes support both protocols. For example, the HTTPBasicAuthenticationScheme only works over HTTP. Pick a BAuthenticationScheme appropriate for the user's requirements.

Details about what a BAuthenticationScheme needs to support authentication over fox or HTTP are described in the authentication documentation.

Categories

All objects designed to be protected by the security model implement the BIProtected interface. The BIProtected interface extends from the BICategorizable interface. An ICategorizable object has the ability to be assigned to one or more categories. In essence a category is just a number: Category 1, Category 2, Category 3, etc. You can give meaningful names categories by mapping category numbers to a BCategory component within the BCategoryService. Most objects of interest implement the BIProtected interface including BComponent, BIFile, and BIHistory.

Categories are just arbitrary groups - you can use categories to model whatever your imagination dreams up. Typically for security they will map to some type of role, for example any device associated with lighting may be assigned to a "lighting" category. But that same device may also be assigned to a "floor3" category.

Categories are implemented as variable length bit strings with each bit representing a category number: bit 1 for Category 1, bit 2 for Category 2, etc. This bit mask is encapsulated via the BCategoryMask class. CategoryMasks are stored and displayed as hex strings, for example the mask for membership in category 2 and 4 would be "a". There are two special CategoryMasks, the "" empty string represents the NULL mask (membership in no categories) and "*" represents the WILDCARD mask (membership in all categories).

The BICategorizable interface provides a getCategoryMask() method to get the configured category mask for the object. However most objects support the notation of category inheritance, where the configured mask is null and the applicable category mask is inherited from an ancestor. This is called the applied category mask and is accessed via the getAppliedCategoryMask() method.

Permissions

Once a user has been authenticated, the user is granted or denied permissions for each protected object in the system using the user's roles' configured BPermissionsMap. This map grants the user permissions for each category, thereby granting the user permissions for objects assigned to that category. Users may be configured as super users by setting their permissions map to BPermissionsMap.SUPER_USER. Super users are automatically granted every permission in every category for every object.

Permission Levels

Niagara defines two permission levels called operator and admin. Each slot in a BComponent is assigned to be operator or admin based on whether the Flags.OPERATOR bit is set.

Permissions

Each slot is defined as admin or operator level. Six permissions are derived to control access to slots:

The BPermissions class is used to store a bitmask of these six permissions.

Component Permission Semantics

The following are the standard semantics applied to BComponents:

OperationOn SlotPermission Required
readoperator non-BComponent propertiesoperatorRead
writeoperator non-BComponent propertiesoperatorWrite
readadmin non-BComponent propertiesadminRead
writeadmin non-BComponent propertiesadminWrite
readoperator BComponent propertiesoperatorRead on child
readadmin BComponent propertiesoperatorRead on child
invokeoperator actionsoperatorInvoke
invokeadmin actionsadminInvoke
readoperator topicsoperatorRead
readadmin topicsadminRead

Note that the permissions required to access a property containing a BComponent are based on the child BComponent regardless of access to its parent or whether the containing slot is marked operator or admin.

File Permission Semantics

BIFiles use the operatorRead permissions to check read access for the file and operatorWrite to check write access. For a directory operatorRead is required to list the directory, and operatorWrite to create a new file.

Computing Permissions

To check the permissions available for a specific object use the BIProtected.getPermissions(Context) method. If working with an OrdTarget, then it is preferable to use OrdTarget.getPermissionsForTarget(), which computes the permissions once and then caches the result.

The standard mechanism to compute permissions by an IProtected object is:

  1. If the Context is null or doesn't specify a user, then return BPermissions.all
  2. Route to BUser.getPermissionsFor(). Note: don't use this method directly, because it might by-pass special cases within IProtected.getPermissionsFor() (see below).
  3. Get the object's mask using getAppliedCategoryMask().
  4. Map the category mask to a permissions mask via BPermissionsMap.getPermissions(BCategoryMask), which is a logical "OR" of each permission assigned to the configured categories.

There are a couple special cases to note. First is that BComponent access requires access to the entire ancestor tree. For example to access "c" in "/a/b/c", requires at least operatorRead access to "a" and "b". The system will automatically grant operatorRead to all ancestors of a component which a user has at least one permission on. Note that this calculation is only done periodically, but can be forced using the CategoryService.update action.

Another special case is BIFile which applies these special rules for file system protection:

  1. Files in a BModule are automatically granted operatorRead (this does not include .class files which are never mapped into the ord name space).
  2. If the user is not a super user, automatically deny any permissions outside of the station home directory
  3. Any remaining cases map to user's configured permissions via the file's categories

Checking Permissions

Permission checks are built-in at several layers of the framework:

Each of these checks is discussed in detail.

BComponent Modification

The following methods will check user permissions if a non-null Context is passed with a non-null BUser. If the permission is not available then a PermissionException is thrown.

Developers should take care to use the proper version of the method with a user context when applicable.

Fox Traffic

Fox is the primary protocol used for workbench-to-station and station-to-station communication. Fox automatically performs all permission checks on the server side before sensitive data can be accessed or modified by a client. By the time a BComponent reaches the client Fox ensures the following:

Furthermore all attempts to modify components are checked by the server being committed.

Workbench Access

Each view declares the permissions a user is required to have on a given BComponent in order to access the view. These permissions are usually declared in the module manifest (module-include). By default views require adminWrite. To override the default:

  
  <type name="PropertySheet" class="com.tridium.workbench.propsheet.BPropertySheet">
    <agent requiredPermissions="r"><on type="baja:Component"/></agent></type>
  

Note that required permissions for a dynamic PxViews are configured via the BPxView.requiredPermissions property.

Auditing

One of the important aspects of security is the ability to analyze what has happened after the fact. The Niagara component model is designed to audit all property modifications and action invocations. Auditable actions include:

Component modifications are only audited when the modification method is passed a non-null Context with a non-null BUser. The history module includes a standard implementation of an audit trail stored to a history database file.

Code Samples

In order to check if a BUser has a operator read permission on specified component:

  
target.getPermissionsFor(user).has(BPermissions.operatorRead) // BUser implements Context
  

This snippet of code will throw a PermissionException if the user lacks the admin invoke permission:

  
user.check(target, BPermissions.adminInvoke)
  

To filter a list of INavNode children for security:

  
BINavNode[] kids = node.getNavChildren();
kids = BNavContainer.filter(kids, context);
  

Use an AccessCursor to automatically skip slots that a user lacks permission to read/invoke:

  
SlotCursor c = AccessSlotCursor.make(target.getSlots(), user)
while(c.next()) {}