Roles are derived dynamically from the state of an AuthorizationGrant (defined in platform/arcus-security/.../authz/AuthorizationGrant.java). There are four roles:
| Role | Condition | Description |
|---|---|---|
OWNER |
accountOwner = true |
Account owner — full access plus billing and account management |
FULL_ACCESS |
Non-owner with non-empty permissions set | Can control devices and access all place features |
HOBBIT |
Non-owner with empty permissions set | Limited access — no device control permissions |
OTHER |
Defined but never returned by getRole() |
Vestigial; unused in practice |
These values are declared in the capability type XMLs (placeaccessdescriptor.xml, personaccessdescriptor.xml).
Permissions use Shiro wildcard syntax with three colon-separated parts:
capability:operation:instanceId
Operations are defined in PermissionCode:
| Code | Meaning |
|---|---|
r |
Read |
w |
Write |
x |
Execute |
c |
Create |
d |
Delete |
Examples:
*:*:*— full access to everything (default for new accounts and invitees)dev:*:*— full access to thedevcapability namespaceswit:x:*— execute commands on any switch devicedev:r:<deviceId>— read-only on a specific device
Instance-specific permissions (where the third segment is a concrete ID) are evaluated before wildcard permissions.
An AuthorizationGrant is a per-person-per-place access record stored in Cassandra. Each grant contains:
- entityId — the person being granted access
- placeId — the place they can access
- accountId — the account that owns the place
- accountOwner — whether this person is the account owner
- permissions — set of Shiro wildcard permission strings
Grants are stored in two Cassandra tables kept in sync via batch writes:
authorization_grant— keyed byentityId(lookup by person)authorization_grant_by_place— keyed byplaceId(lookup by place)
When an account is created, the owner gets accountOwner=true with permissions=["*:*:*"]. When an invitation is accepted, the invitee gets accountOwner=false with permissions=["*:*:*"] (FULL_ACCESS).
Every platform message passes through two authorization layers:
Layer 1 — RoleAuthorizer (platform/arcus-security/.../authz/RoleAuthorizer.java):
Coarse-grained checks based on message type:
- Certain messages (e.g.,
IssueInvoiceRefundRequest) are always denied to end users (reserved for internal support) - Account-mutating messages (billing, place deletion) require
accountOwner=true - "Self" methods (add mobile device, delete login) require the message actor to match the destination
- Place-scoped service requests verify the user has at least one permission for the target place
Layer 2 — PermissionsAuthorizer (platform/arcus-security/.../authz/PermissionsAuthorizer.java):
Fine-grained Shiro permission checking:
- Extracts required permissions from the message via
PermissionExtractorRegistry - Checks instance-specific permissions first (per-device overrides), then wildcard permissions
At WebSocket session establishment, SubscriberAuthorizationContextLoader builds an AuthorizationContext by loading all AuthorizationGrants for the authenticated user from Cassandra. This context caches parsed permissions per place:
- Instance permissions (device-specific grants)
- Non-instance permissions (namespace-wide grants)
Authentication uses Apache Shiro with GuicedIrisRealm as the primary realm:
- Client submits email + password
AuthenticationDAO.findLogin(email)retrieves the stored hash and salt from theloginCassandra table- Shiro's
HashedCredentialsMatcherre-hashes the submitted password with the stored salt and compares - On success, a
DefaultPrincipal(username, userId)is created - A Shiro session is persisted to Cassandra via
GuicedCassandraSessionDAO
A secondary AppHandoffRealm handles app-to-app handoff (e.g., mobile app to web). It accepts an AppHandoffToken validated against the app_handoff_token Cassandra table rather than by credential matching.
Passwords are hashed using SHA-256 with a random salt and 1024 iterations, Base64-encoded.
Implementation: platform/arcus-security/.../credentials/Sha256CredentialsHashingStrategy.java
// Hashing
new Sha256Hash(credentials, salt, 1024).toBase64()
// Salt generation
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
return rng.nextBytes();The login Cassandra table stores:
| Column | Description |
|---|---|
domain |
Email domain (e.g., gmail.com) |
user_0_3 |
First 4 chars of local part (bucketing) |
user |
Full local part of email |
password |
SHA-256 hash, Base64-encoded |
password_salt |
Random salt, Base64-encoded |
personid |
UUID foreign key to person table |
reset_token |
Temporary reset token (TTL, default 15 min) |
lastPassChange |
Timestamp of last password change |
PINs are stored per-place in person.pinPerPlace (a map of placeId to encrypted PIN), encrypted using AES. Security question answers are also AES-encrypted.
| File | Purpose |
|---|---|
platform/arcus-security/.../authz/AuthorizationGrant.java |
Role derivation, permission storage |
platform/arcus-security/.../credentials/Sha256CredentialsHashingStrategy.java |
Password hashing (SHA-256, 1024 iterations) |
platform/arcus-security/.../GuicedIrisRealm.java |
Primary Shiro realm (username + password) |
platform/arcus-security/.../handoff/AppHandoffRealm.java |
Secondary Shiro realm (app handoff) |
platform/arcus-security/.../SecurityModule.java |
Guice wiring of Shiro components |
platform/arcus-security/.../authz/AuthorizationContext.java |
Per-session permission cache |
platform/arcus-security/.../authz/RoleAuthorizer.java |
Coarse-grained role/ownership checks |
platform/arcus-security/.../authz/PermissionsAuthorizer.java |
Fine-grained Shiro wildcard permission checks |
platform/arcus-security/.../authz/permission/PermissionCode.java |
Permission codes: r, w, x, c, d |
platform/arcus-lib/.../PersonDAOImpl.java |
Password hash/salt storage in Cassandra |
platform/arcus-lib/.../AuthorizationGrantDAOImpl.java |
Grant persistence (two Cassandra tables) |
./gradlew -Peyeris_owasp=1 :platform:arcus-containers:client-bridge:dependencyCheckAnalyze