Skip to main content
Version: 4.12.0

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​

LayerAPIPurpose
RolesRole()Define personas with permissions
ACLsAcl()Control access to objects/operations
Security AttributesRecord on sys_security_attributeReusable security predicates
Data FiltersRecord on sys_security_data_filterRow-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), reference them by their role name string (e.g., 'itil', 'sn_customerservice_manager') -- never use the sys_id.
  4. 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, "itil"]
// Fluent object and OOB role name
});

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).
  6. After creating an ACL rule, you cannot change the object type -- delete the ACL and create a new one with the correct type.

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. A Deny-Unless ACL is not evaluated if the user is already denied by another ACL.

// 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 by controlling which query operators a user can apply to a column. Use when columns contain sensitive values and some users have partial/conditional access.

Default behavior: Both query_match and query_range default to a *.* ACL that delegates to read access. Where no query ACL exists, read-access rules apply; where query ACLs are defined, they override the default.

query_match -- controls "safe" operators that fetch specific records: EQUALS, NOT_EQUALS, IN, NOT_IN, SAMEAS, NSAMEAS, ANYTHING, ISEMPTYSTRING, ISEMPTY, ISNOTEMPTY, ISNULL, ISNOTNULL

query_range -- controls range/pattern operators that can be exploited to extract data: STARTS_WITH, CONTAINS, >=, <=, BETWEEN, and similar range operators. Sorting by the column is also restricted when this ACL fails.

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​

TypeUse forCan use in ACLs?
compoundRole/group conditions via encoded queryYes
true|falseComplex boolean logic via scriptNo
string / integer / listValue calculationsNo

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​

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​

FieldTypeRequiredDescription
descriptionstringYesDescriptive name.
table_namestringYesTarget table.
modestringYes"if" (filter when condition met) or "unless" (filter unless condition met).
security_attributereferenceYesReference to sys_security_attribute (must be compound type).
filterstringNoEncoded query condition.
activebooleanNoDefault: 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​

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