@servicenow/sdk
Version:
ServiceNow SDK
285 lines (222 loc) • 9.41 kB
Markdown
---
tags: [security, acl, access, access-control, role, permissions, data-filter, security-attribute, sys_security_acl, sys_user_role]
---
# Implementing Security Guide
Implement ServiceNow application security using ACLs (`sys_security_acl`), Roles (`sys_user_role`), Security Attributes (`sys_security_attribute`), and Security Data Filters (`sys_security_data_filter`). This guide covers the layered security model -- from defining roles, to creating ACL rules, to row-level filtering with data filters and reusable security predicates.
## When to Use
- Securing tables, fields, or resources with access control rules
- Creating roles for an application
- Implementing row-level data filtering based on user attributes
- Defining reusable security predicates (Security Attributes)
- Proactively when creating tables with sensitive data or applications needing role-based access control
## Overall Security Model
1. **Start with Roles:** Define roles first -- they are required by ACLs and referenced by Security Attributes.
2. **Then ACLs:** Create ACL rules to secure tables, fields, and resources. Each ACL secures one operation on one object.
3. **Use Security Attributes for reusable predicates:** When the same role/condition logic appears in multiple ACLs, extract it into a Security Attribute.
4. **Add Data Filters for row-level security:** When users should see only certain rows (not the whole table), add Security Data Filters paired with Deny ACLs.
## Security Layer Hierarchy
| Layer | API | Purpose |
|-------|-----|---------|
| Roles | `Role()` | Define personas with permissions |
| ACLs | `Acl()` | Control access to objects/operations |
| Security Attributes | `Record` on `sys_security_attribute` | Reusable security predicates |
| Data Filters | `Record` on `sys_security_data_filter` | Row-level filtering |
## ACL Evaluation Order
1. Deny-Unless ACLs evaluate first -- if any fail, access is denied
2. Allow-If ACLs evaluate second -- at least one must pass to grant access
3. Within each ACL: roles, condition, and script ALL must pass (the "Trinity")
## Roles
### Instructions
1. Always prefix role names with the application scope -- e.g., `x_my_scope.manager`.
2. Use `containsRoles` for inheritance -- a supervisor role can contain a manager role.
3. For existing platform roles (e.g., `itil`), look up the `sys_id` from `sys_user_role`.
4. You cannot rename roles after they are saved.
### Role API Reference
For the full property reference, see the `role-api` topic.
### Role Example
```javascript
import { Role } from "@servicenow/sdk/core";
const managerRole = Role({
$id: Now.ID["manager_role"],
name: "x_snc_example.manager"
});
const adminRole = Role({
$id: Now.ID["admin_role"],
name: "x_snc_example.admin",
containsRoles: [managerRole]
});
const supervisorRole = Role({
$id: Now.ID["supervisor_role"],
name: "x_snc_example.supervisor",
containsRoles: [managerRole, "282bf1fac6112285017366cb5f867469"]
// Fluent object and sys_id of itil role
});
```
## ACLs
### Instructions
1. ACLs require at least one of: roles, security attribute, condition, or script.
2. **The Trinity:** All specified conditions (roles AND condition AND script) must evaluate to true to grant access.
3. For table-level access: Use `type: 'record'` and omit `field`. For field-level, set `field` to the column name or `"*"` for all fields.
4. One ACL per operation: Create separate ACLs for read, write, delete, etc.
5. Use `roles` for role checks, not scripts -- only use scripts for complex business logic (e.g., ownership checks).
### ACL API Reference
For the full property reference, see the `acl-api` topic.
### ACL Examples
**Table-level ACL with roles:**
```javascript
import { Acl, Role } from "@servicenow/sdk/core";
const travelAgentRole = Role({
$id: Now.ID["travel_agent_role"],
name: "sn_travel_app.travel_agent"
});
export default Acl({
$id: Now.ID["booking_read_acl"],
type: "record",
table: "sn_travel_app_booking",
operation: "read",
roles: [travelAgentRole],
adminOverrides: true
});
```
**Field-level ACL:**
```javascript
export default Acl({
$id: Now.ID["booking_status_write_acl"],
type: "record",
table: "sn_travel_app_booking",
field: "status",
operation: "write",
roles: [travelAgentRole, travelManagerRole],
});
```
**Script-based ACL (ownership check):**
```javascript
export default Acl({
$id: Now.ID["booking_delete_owner_acl"],
type: "record",
table: "sn_travel_app_booking",
operation: "delete",
roles: [travelerRole],
script: `
var isOwner = (current.sys_created_by == gs.getUserName());
var isPending = (current.status == 'pending');
answer = isOwner && isPending;
`,
});
```
### Deny-Unless ACLs
Deny-Unless ACLs (`decisionType: 'deny'`) evaluate before Allow ACLs and deny access unless conditions are met. They do not grant access on their own -- at least one Allow ACL must also match.
```javascript
// Deny access unless user has itil role
export const incidentDenyUnlessItil = Acl({
$id: Now.ID["incident_deny_unless_itil"],
type: "record",
table: "incident",
operation: "read",
decisionType: "deny",
roles: [itilRole],
});
// Corresponding Allow ACL
export const incidentAllowRead = Acl({
$id: Now.ID["incident_allow_read"],
type: "record",
table: "incident",
operation: "read",
decisionType: "allow",
roles: [itilRole],
});
```
### Query ACLs
Query ACLs protect against blind query attacks:
- `query_match` -- Controls safe operators (EQUALS, IN, NOT_EQUALS, etc.)
- `query_range` -- Controls dangerous operators (CONTAINS, >=, <=, STARTS_WITH, etc.)
Use when columns contain sensitive values and partial/conditional access exists.
```javascript
export const payrollSalaryQueryRange = Acl({
$id: Now.ID["payroll_salary_query_range"],
type: "record",
table: "payroll",
field: "salary",
operation: "query_range",
decisionType: "deny",
roles: [hrAdminRole],
});
```
## Security Attributes
Use the Record API on `sys_security_attribute`. Prefer `compound` type -- it is the only type that can be referenced in ACLs and Data Filters.
### Security Attribute Types
| Type | Use for | Can use in ACLs? |
|------|---------|-----------------|
| `compound` | Role/group conditions via encoded query | Yes |
| `true\|false` | Complex boolean logic via script | No |
| `string` / `integer` / `list` | Value calculations | No |
### Key Rules
- Use `condition` field for compound types with encoded query syntax (e.g., `"Role=manager^ORRole=admin"`)
- Never use `current` in security attribute scripts -- no record context available
- Set `is_dynamic: false` for role/group checks that can be cached per session
### Examples
```typescript fluent
import "@servicenow/sdk/global";
import { Record } from "@servicenow/sdk/core";
// Compound type (recommended)
export const hasManagerRole = Record({
$id: Now.ID["has-manager-role"],
table: "sys_security_attribute",
data: {
name: "HasManagerRole",
type: "compound",
label: "Has Manager Role",
description: "Checks if the current user has the manager role",
condition: "Role=manager",
is_dynamic: false
}
});
// Boolean script type
export const hasFinanceRole = Record({
$id: Now.ID["has-finance-role"],
table: "sys_security_attribute",
data: {
name: "HasFinanceRole",
type: "boolean",
label: "Has Finance Role",
script: 'answer = gs.hasRole("finance") || gs.getUser().isMemberOf("finance_users");',
is_dynamic: false
}
});
```
## Security Data Filters
Use the Record API on `sys_security_data_filter`. Always pair with Deny ACLs -- Data Filters alone do not provide complete security.
### Properties
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | `string` | Yes | Descriptive name. |
| `table_name` | `string` | Yes | Target table. |
| `mode` | `string` | Yes | `"if"` (filter when condition met) or `"unless"` (filter unless condition met). |
| `security_attribute` | `reference` | Yes | Reference to `sys_security_attribute` (must be compound type). |
| `filter` | `string` | No | Encoded query condition. |
| `active` | `boolean` | No | Default: `true`. |
### Key Rules
- `security_attribute` is required -- always reference a compound Security Attribute
- Use dynamic conditions (e.g., `fieldnameDYNAMIC90d1921e5f510100a9ad2572f2b477fe` for current user) instead of hardcoded values
- Use indexed columns in filters for performance
### Example
```typescript fluent
export const filterFinancialRecords = Record({
$id: Now.ID["filter-financial-records"],
table: "sys_security_data_filter",
data: {
description: "Restrict high-value transactions to authorized personnel",
table_name: "finance_transaction",
mode: "unless",
security_attribute: hasFinanceRoleAttribute,
filter: "amount>10000^ORclassification=confidential",
}
});
```
## Avoidance
- Never create roles without scope prefix -- use `x_scope.role_name` format
- Never use scripts in ACLs for simple role checks -- use the `roles` property
- Never rely on Data Filters alone -- always pair with Deny ACLs
- Never use `current` in Security Attribute scripts -- no record context available
- Never hardcode user IDs or names in ACL scripts or Data Filter conditions
- Never use non-compound Security Attributes in ACLs -- only compound type is supported