ACL Management
Overview
Access Control Lists (ACLs) are the foundation of ServiceNow security. This skill covers:
- Understanding ACL structure and inheritance hierarchy
- Creating new ACLs for tables, fields, and records
- Modifying existing ACL rules safely
- Troubleshooting permission issues
- ACL debugging techniques and tools
- Best practices for role-based access control
When to use:
- Setting up security for new tables or applications
- Troubleshooting "access denied" issues
- Implementing field-level security
- Auditing and reviewing existing permissions
- Implementing row-level security (query conditions)
Prerequisites
- Roles:
security_adminoradmin - Access: sys_security_acl, sys_security_acl_role tables
- Knowledge: Understanding of ServiceNow roles and user groups
- Plugins: Core platform (ACLs are built-in)
ACL Fundamentals
ACL Hierarchy
┌─────────────────┐
│ Table ACL (*) │ ← Broadest
│ incident.* │
└────────┬────────┘
│
┌────────▼────────┐
│ Table.Field │ ← Field-specific
│ incident.state │
└────────┬────────┘
│
┌────────▼────────┐
│ Table.None │ ← Row-level
│ (with script) │
└─────────────────┘ ← Most specific
ACL Operations
| Operation | Purpose | Common Use |
|-----------|---------|------------|
| read | View records/fields | List, form display |
| write | Modify records/fields | Form editing |
| create | Insert new records | New record creation |
| delete | Remove records | Record deletion |
| execute | Run scripts/actions | UI actions, processors |
ACL Evaluation Order
- Most specific wins: Field ACL > Table ACL
- First match: Evaluation stops at first matching ACL
- Deny by default: No matching ACL = access denied
- Admin bypass: Admin role bypasses most ACLs
Procedure
Phase 1: Understanding Existing ACLs
1.1 Query ACLs for a Table
Find all ACLs for a specific table:
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl
query: nameLIKEincident
fields: name,operation,type,condition,script,active
limit: 50
Find ACLs by operation type:
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl
query: name=incident^operation=write
fields: name,operation,admin_overrides,condition,script,active
limit: 20
1.2 Discover ACL Structure
Get the ACL table schema:
Tool: SN-Get-Table-Schema
Parameters:
table_name: sys_security_acl
Key ACL Fields:
| Field | Type | Purpose |
|-------|------|---------|
| name | String | Table.field or Table.* |
| operation | Choice | read, write, create, delete, execute |
| type | Choice | record, client_callable_script_include |
| condition | Condition | Filter conditions for row-level security |
| script | Script | Advanced condition logic |
| admin_overrides | Boolean | Allow admin bypass |
| active | Boolean | ACL is enforced |
| advanced | Boolean | Uses script conditions |
1.3 Check ACL Role Requirements
Find roles required for an ACL:
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl_role
query: acl.name=incident.*^acl.operation=write
fields: acl.name,acl.operation,role.name
limit: 20
Phase 2: Creating New ACLs
2.1 Create Table-Level ACL
Create read ACL for custom table:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_custom_table.*"
operation: read
type: record
admin_overrides: true
active: true
description: "Read access to custom table"
Add role requirement:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl_role
data:
acl: [acl_sys_id]
role: [role_sys_id]
2.2 Create Field-Level ACL
Protect a sensitive field:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_custom_table.ssn_number"
operation: read
type: record
admin_overrides: true
active: true
description: "Restrict SSN field to authorized users only"
Add multiple role requirements:
Tool: SN-Query-Table
Parameters:
table_name: sys_user_role
query: nameINhr_admin,security_admin
fields: sys_id,name
limit: 10
Then for each role:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl_role
data:
acl: [field_acl_sys_id]
role: [hr_admin_role_sys_id]
2.3 Create Row-Level ACL (Condition-Based)
Users can only see their own records:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_custom_table"
operation: read
type: record
condition: "opened_by=javascript:gs.getUserID()"
admin_overrides: true
active: true
description: "Users can only read their own records"
2.4 Create Script-Based ACL
Complex logic requiring script:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_custom_table"
operation: write
type: record
advanced: true
script: |
// Allow write if user is the owner OR is in the assigned group
var userId = gs.getUserID();
var userGroups = gs.getUser().getMyGroups();
if (current.owner == userId) {
answer = true;
} else if (userGroups.indexOf(current.assignment_group.toString()) >= 0) {
answer = true;
} else {
answer = false;
}
admin_overrides: true
active: true
description: "Allow write for owner or group members"
Phase 3: Modifying ACLs Safely
3.1 Deactivate Before Major Changes
Deactivate ACL temporarily:
Tool: SN-Update-Record
Parameters:
table_name: sys_security_acl
sys_id: [acl_sys_id]
data:
active: false
Make changes:
Tool: SN-Update-Record
Parameters:
table_name: sys_security_acl
sys_id: [acl_sys_id]
data:
condition: "state!=6^ORassigned_to=javascript:gs.getUserID()"
description: "Updated condition - users can access non-closed or own records"
Reactivate after testing:
Tool: SN-Update-Record
Parameters:
table_name: sys_security_acl
sys_id: [acl_sys_id]
data:
active: true
3.2 Add/Remove Role Requirements
Add a new role requirement:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl_role
data:
acl: [acl_sys_id]
role: [new_role_sys_id]
Remove a role requirement:
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl_role
query: acl=[acl_sys_id]^role=[role_sys_id]
fields: sys_id
# Then delete the record using SN-Update-Record or background script
Phase 4: Troubleshooting Permission Issues
4.1 Identify the Blocking ACL
Enable ACL Debugging (Session-Based):
Tool: SN-Execute-Background-Script
Parameters:
script: |
// This enables debug output for the session
// Run manually: Navigate to User menu > Impersonate > Check security
gs.info('To debug ACLs:');
gs.info('1. Navigate to System Security > Debugging');
gs.info('2. Or add ?sysparm_security_debug=true to URL');
gs.info('3. Or impersonate user and check security diagnostics');
description: "ACL debugging instructions"
Check user's roles:
Tool: SN-Query-Table
Parameters:
table_name: sys_user_has_role
query: user=[user_sys_id]^role.name!=snc_internal
fields: role.name,granted_by,inherited
limit: 50
Check user's groups:
Tool: SN-Query-Table
Parameters:
table_name: sys_user_grmember
query: user=[user_sys_id]
fields: group.name,group.sys_id
limit: 50
4.2 Trace ACL Evaluation
Find which ACL applies:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var table = 'incident';
var operation = 'write';
var fieldName = 'state'; // or '*' for table-level
var acl = new GlideRecord('sys_security_acl');
acl.addQuery('name', 'CONTAINS', table);
acl.addQuery('operation', operation);
acl.addQuery('active', true);
acl.orderBy('name');
acl.query();
gs.info('ACLs for ' + table + '.' + fieldName + ' (' + operation + '):');
while (acl.next()) {
var roles = [];
var roleGr = new GlideRecord('sys_security_acl_role');
roleGr.addQuery('acl', acl.sys_id);
roleGr.query();
while (roleGr.next()) {
roles.push(roleGr.role.name.toString());
}
gs.info(acl.name + ' - Roles: [' + roles.join(', ') + '] - Condition: ' + acl.condition);
}
description: "Trace ACL evaluation for table/operation"
4.3 Test User Access
Check if user has required roles:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var userId = '[user_sys_id]';
var requiredRoles = ['itil', 'incident_manager'];
var user = new GlideRecord('sys_user');
user.get(userId);
gs.info('Checking roles for: ' + user.user_name);
for (var i = 0; i < requiredRoles.length; i++) {
var hasRole = new GlideRecord('sys_user_has_role');
hasRole.addQuery('user', userId);
hasRole.addQuery('role.name', requiredRoles[i]);
hasRole.query();
if (hasRole.hasNext()) {
gs.info(' HAS: ' + requiredRoles[i]);
} else {
gs.warn(' MISSING: ' + requiredRoles[i]);
}
}
description: "Check user role membership"
4.4 Common Permission Issues
Issue: User can see table but not specific records
# Check for row-level ACL conditions
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl
query: name=[table_name]^operation=read^conditionISNOTEMPTY
fields: name,condition,script
Issue: User can read but not write
# Compare read vs write ACLs
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl
query: nameLIKE[table_name]^operationINread,write
fields: name,operation,condition
limit: 20
Issue: Field is read-only unexpectedly
# Check field-level ACLs
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl
query: name=[table_name].[field_name]^operation=write
fields: name,operation,condition,active
Phase 5: ACL Debugging Tools
5.1 Security Diagnostic Tool
Run security diagnostic for a record:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var table = 'incident';
var sysId = '[record_sys_id]';
var userId = '[user_sys_id]';
// Impersonate and check access
var gr = new GlideRecord(table);
gr.get(sysId);
var canRead = gr.canRead();
var canWrite = gr.canWrite();
var canDelete = gr.canDelete();
gs.info('Security Diagnostic for ' + table + '/' + sysId);
gs.info(' Can Read: ' + canRead);
gs.info(' Can Write: ' + canWrite);
gs.info(' Can Delete: ' + canDelete);
// Check field-level access
var elements = ['state', 'priority', 'assigned_to'];
for (var i = 0; i < elements.length; i++) {
var field = elements[i];
gs.info(' Field ' + field + ' - canRead: ' + gr[field].canRead() +
', canWrite: ' + gr[field].canWrite());
}
description: "Run security diagnostic on record"
5.2 ACL Rule Analyzer
Analyze all ACLs for conflicts:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var table = 'incident';
var analysis = {
table_acls: [],
field_acls: [],
potential_conflicts: []
};
var acl = new GlideRecord('sys_security_acl');
acl.addQuery('name', 'STARTSWITH', table);
acl.addQuery('active', true);
acl.orderBy('name');
acl.query();
while (acl.next()) {
var aclInfo = {
name: acl.name.toString(),
operation: acl.operation.toString(),
has_condition: acl.condition.toString() !== '',
has_script: acl.advanced.toString() === 'true'
};
if (acl.name.toString().indexOf('.') === -1 ||
acl.name.toString().endsWith('.*')) {
analysis.table_acls.push(aclInfo);
} else {
analysis.field_acls.push(aclInfo);
}
}
// Check for potential issues
if (analysis.table_acls.length === 0) {
analysis.potential_conflicts.push('No table-level ACLs - may be too open or rely on inheritance');
}
gs.info('ACL Analysis for ' + table + ':\n' + JSON.stringify(analysis, null, 2));
description: "Analyze ACLs for conflicts and issues"
5.3 Role Elevation Check
Find users with potentially dangerous role combinations:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var dangerousCombos = [
['admin', 'maint'],
['security_admin', 'personalize_dictionary'],
['admin', 'soap']
];
var findings = [];
for (var i = 0; i < dangerousCombos.length; i++) {
var combo = dangerousCombos[i];
var users = new GlideRecord('sys_user');
users.addQuery('active', true);
users.query();
while (users.next()) {
var hasAll = true;
for (var j = 0; j < combo.length; j++) {
var roleCheck = new GlideRecord('sys_user_has_role');
roleCheck.addQuery('user', users.sys_id);
roleCheck.addQuery('role.name', combo[j]);
roleCheck.query();
if (!roleCheck.hasNext()) {
hasAll = false;
break;
}
}
if (hasAll) {
findings.push({
user: users.user_name.toString(),
roles: combo
});
}
}
}
if (findings.length > 0) {
gs.warn('Users with dangerous role combinations:');
gs.info(JSON.stringify(findings, null, 2));
} else {
gs.info('No dangerous role combinations found');
}
description: "Find users with dangerous role combinations"
Best Practices
Design Principles
- Principle of Least Privilege: Grant minimum access needed
- Role-Based Access: Use roles, not individual users
- Inherit When Possible: Use table-level ACLs as baseline
- Document Everything: Add descriptions to all ACLs
- Test Thoroughly: Verify with multiple user roles
Common Patterns
Pattern 1: Owner + Group Access
// Condition for owner or group member access
opened_by=javascript:gs.getUserID()^ORassignment_group.memberSOME=javascript:gs.getUserID()
Pattern 2: Hierarchical Access
// Managers can see their team's records
var mgr = new GlideRecord('sys_user');
mgr.addQuery('manager', gs.getUserID());
mgr.query();
var users = [gs.getUserID()];
while (mgr.next()) users.push(mgr.sys_id.toString());
answer = gs.getUser().getID() in users.join(',').split(',');
Pattern 3: Time-Based Access
// Only allow access during business hours
var now = new GlideDateTime();
var hour = now.getLocalTime().getHourOfDay();
answer = (hour >= 8 && hour < 18);
Tool Usage Summary
| Operation | MCP Tool | REST Endpoint | |-----------|----------|---------------| | Query ACLs | SN-Query-Table | GET /sys_security_acl | | Create ACL | SN-Create-Record | POST /sys_security_acl | | Update ACL | SN-Update-Record | PATCH /sys_security_acl | | Add Role | SN-Create-Record | POST /sys_security_acl_role | | Debug ACL | SN-Execute-Background-Script | Via sys_trigger | | Check Schema | SN-Get-Table-Schema | GET /sys_dictionary |
Troubleshooting
ACL Not Being Evaluated
Symptom: Changes to ACL have no effect Cause: ACL cache not cleared or ACL inactive Solution:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Clear ACL cache
GlideAccessControl.reset();
gs.info('ACL cache cleared');
description: "Clear ACL cache"
Admin Can't Access Record
Symptom: Admin role blocked from record Cause: ACL has admin_overrides=false Solution:
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl
query: name=[table]^admin_overrides=false
fields: name,operation,admin_overrides
Script ACL Always Fails
Symptom: Script-based ACL never grants access Cause: Script error or incorrect answer variable Solution:
- Verify script sets
answer = trueoranswer = false - Check for script errors in syslog
- Ensure
advanced = trueis set
Role Not Granting Access
Symptom: User has role but still denied Cause: ACL requires multiple roles (AND) or condition fails Solution:
Tool: SN-Query-Table
Parameters:
table_name: sys_security_acl_role
query: acl=[acl_sys_id]
fields: role.name
All listed roles are required (AND logic by default).
Examples
Example 1: Secure a Custom Table
# Step 1: Create table-level read ACL
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_employee_data.*"
operation: read
type: record
admin_overrides: true
active: true
description: "Base read access to employee data"
# Step 2: Add role requirement
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl_role
data:
acl: [step1_sys_id]
role: [hr_user_role_sys_id]
# Step 3: Create write ACL (more restrictive)
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_employee_data.*"
operation: write
type: record
admin_overrides: true
active: true
description: "Write access to employee data - HR admins only"
# Step 4: Add admin role requirement
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl_role
data:
acl: [step3_sys_id]
role: [hr_admin_role_sys_id]
Example 2: Field-Level SSN Protection
# Protect SSN field - only security cleared users
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "u_employee_data.ssn"
operation: read
type: record
admin_overrides: false
active: true
description: "SSN access restricted - security clearance required"
# Add special role
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl_role
data:
acl: [ssn_acl_sys_id]
role: [security_cleared_role_sys_id]
Related Skills
security/audit-compliance- Auditing ACL changessecurity/incident-response- Responding to access violationssecurity/data-classification- Identifying what needs protectionadmin/user-provisioning- Managing user roles
Scan to join WeChat group