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.
  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 a human user, 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 default implementation of BUserService simply stores the system users as dynamic slots. You can alternatively use BLdapUserService to lookup users via the LDAP protocol.

BUser is used to store the authentication credentials, permissions, 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 and Encryption

All authentication in the Niagara framework is based on the BUserService configured for a station database. The BUserService is used to lookup BUsers by username. The BUser is then used to check passwords and security permissions. Encryption is usually negotiated at authentication time.

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 password which is used to authenticate the Fox connection.
  2. Fox Station to Station: When a connection is made from a station to another station, a preconfigured username/password is 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.

Fox Authentication

The Fox authentication mechanism is configurable via the FoxService (usually under the NiagaraNetwork). Authentication can be either basic or digest. Digest is the preferred mechanism since the password is never passed in cleartext. However if using LDAP, then basic authentication must be used.

HTTP Authentication

The BWebService is used to manage HTTP requests to a Niagara station. The HTTP realm is always the value of BStation.stationName. The default authentication mechanism used by the WebService is cookie based authentication. Cookie authentication is required if using the workbench with the Java plugin. If you are deploying an application without web workbench support, you can change the authentication mechaism via the WebService.authenticationScheme property.

You can use the guest account to provide access to some or all of your station without requiring a web login. If the guest account is enabled, then any object the guest has operator read access to may be accessed via the web UI without a logon. As soon as an object is accessed which guest does not have operator read on, then the logon challenge is issued. Set the guest user to disabled to turn this feature off.

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 essense 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 inheritence, 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 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 asssigned 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()) {}