Security in the Niagara framework covers a couple of broad topics:
The following steps are used to setup a Niagara security model:
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.
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:
NiagaraStation.clientConnection
component.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.
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.
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.
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.
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.
The following are the standard semantics applied to BComponents
:
Operation | On Slot | Permission Required |
---|---|---|
read | operator non-BComponent properties | operatorRead |
write | operator non-BComponent properties | operatorWrite |
read | admin non-BComponent properties | adminRead |
write | admin non-BComponent properties | adminWrite |
read | operator BComponent properties | operatorRead on child |
read | admin BComponent properties | operatorRead on child |
invoke | operator actions | operatorInvoke |
invoke | admin actions | adminInvoke |
read | operator topics | operatorRead |
read | admin topics | adminRead |
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.
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.
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:
BPermissions.all
BUser.getPermissionsFor()
. Note: don't use this
method directly, because it might by-pass special cases within
IProtected.getPermissionsFor()
(see below).getAppliedCategoryMask()
.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:
Permission checks are built-in at several layers of the framework:
Each of these checks is discussed in detail.
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 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.
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.
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.
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()) {}
Copyright © 2000-2019 Tridium Inc. All rights reserved.