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
- Start with Roles: Define roles first -- they are required by ACLs and referenced by Security Attributes.
- Then ACLs: Create ACL rules to secure tables, fields, and resources. Each ACL secures one operation on one object.
- Use Security Attributes for reusable predicates: When the same role/condition logic appears in multiple ACLs, extract it into a Security Attribute.
- 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
- Deny-Unless ACLs evaluate first -- if any fail, access is denied
- Allow-If ACLs evaluate second -- at least one must pass to grant access
- Within each ACL: roles, condition, and script ALL must pass (the "Trinity")
Roles
Instructions
- Always prefix role names with the application scope -- e.g.,
x_my_scope.manager. - Use
containsRolesfor inheritance -- a supervisor role can contain a manager role. - For existing platform roles (e.g.,
itil), look up thesys_idfromsys_user_role. - You cannot rename roles after they are saved.
Role API Reference
For the full property reference, see the role-api topic.
Role Example
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
- ACLs require at least one of: roles, security attribute, condition, or script.
- The Trinity: All specified conditions (roles AND condition AND script) must evaluate to true to grant access.
- For table-level access: Use
type: 'record'and omitfield. For field-level, setfieldto the column name or"*"for all fields. - One ACL per operation: Create separate ACLs for read, write, delete, etc.
- Use
rolesfor 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:
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:
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):
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.
// 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.
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
conditionfield for compound types with encoded query syntax (e.g.,"Role=manager^ORRole=admin") - Never use
currentin security attribute scripts -- no record context available - Set
is_dynamic: falsefor role/group checks that can be cached per session
Examples
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: "true|false",
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_attributeis required -- always reference a compound Security Attribute- Use dynamic conditions (e.g.,
fieldnameDYNAMIC90d1921e5f510100a9ad2572f2b477fefor current user) instead of hardcoded values - Use indexed columns in filters for performance
Example
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_nameformat - Never use scripts in ACLs for simple role checks -- use the
rolesproperty - Never rely on Data Filters alone -- always pair with Deny ACLs
- Never use
currentin 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