ACL Security Patterns for ServiceNow
Access Control Lists (ACLs) are the foundation of ServiceNow security. They control who can read, write, create, and delete records.
ACL Evaluation Order
ACLs are evaluated in this order (first match wins):
- •Table.field - Most specific (e.g.,
incident.assignment_group) - •*Table.` - Table-level field wildcard
- •Table - Table-level record ACL
- •Parent table ACLs - If table extends another
- •
*- Global wildcard (catch-all)
ACL Types
| Type | Controls | Example |
|---|---|---|
| record | Row-level access | Can user see this incident? |
| field | Field-level access | Can user see assignment_group? |
| client_callable_script_include | Script Include access | Can user call this API? |
| ui_page | UI Page access | Can user view this page? |
| rest_endpoint | REST API access | Can user call this endpoint? |
Creating ACLs via MCP
javascript
// Table-level READ ACL
snow_create_acl({
name: "incident",
operation: "read",
admin_overrides: true,
active: true,
roles: ["itil", "incident_manager"],
condition: "current.active == true",
script: "",
})
// Field-level WRITE ACL
snow_create_acl({
name: "incident.priority",
operation: "write",
roles: ["incident_manager"],
condition: "",
script: "answer = current.state < 6;", // Only if not resolved
})
Common ACL Patterns
Pattern 1: Role-Based Access
javascript
// Condition: (empty - role check only) // Roles: itil, incident_manager // Script: (empty) // Users with itil OR incident_manager role can access
Pattern 2: Ownership-Based Access
javascript
// Condition: current.caller_id == gs.getUserID() || current.assigned_to == gs.getUserID() || current.opened_by == gs.getUserID() // User can access their own records
Pattern 3: Group-Based Access
javascript
// Script:
;(function () {
var userGroups = gs.getUser().getMyGroups()
answer = userGroups.indexOf(current.assignment_group.toString()) >= 0
})()
// User can access records assigned to their groups
Pattern 4: Manager Chain Access
javascript
// Script:
;(function () {
var callerManager = current.caller_id.manager
var currentUser = gs.getUserID()
// Check if current user is in caller's management chain
while (callerManager && !callerManager.nil()) {
if (callerManager.toString() == currentUser) {
answer = true
return
}
callerManager = callerManager.manager
}
answer = false
})()
Pattern 5: Time-Based Access
javascript
// Script:
;(function () {
var now = new GlideDateTime()
var hour = parseInt(now.getLocalTime().getHourOfDayLocalTime())
// Only allow access during business hours (8 AM - 6 PM)
answer = hour >= 8 && hour < 18
})()
Pattern 6: Data Classification
javascript
// Script:
;(function () {
var classification = current.u_data_classification.toString()
var userClearance = gs.getUser().getRecord().getValue("u_security_clearance")
var levels = { public: 0, internal: 1, confidential: 2, secret: 3 }
answer = levels[userClearance] >= levels[classification]
})()
Field-Level Security Patterns
Hide Sensitive Fields
javascript
// ACL: incident.u_ssn (Social Security Number)
// Operation: read
// Script:
answer = gs.hasRole("hr_admin")
// Only HR admins can see SSN field
Read-Only After State Change
javascript
// ACL: incident.short_description // Operation: write // Script: answer = current.state < 6 // Can't edit after Resolved // Prevent editing after resolution
Conditional Field Visibility
javascript
// ACL: incident.u_internal_notes
// Operation: read
// Condition:
gs.hasRole("itil") || current.caller_id == gs.getUserID()
// ITIL users see all, callers see their own
Security Best Practices
1. Principle of Least Privilege
javascript
// ❌ BAD - Too permissive // Roles: (empty) - allows everyone // ✅ GOOD - Explicit roles // Roles: itil, incident_manager
2. Deny by Default
javascript
// Create a catch-all deny ACL at lowest priority // Name: * // Operation: read // Condition: false // This ensures anything not explicitly allowed is denied
3. Avoid Complex Scripts
javascript
// ❌ BAD - Complex script ACL (slow)
;(function () {
var gr = new GlideRecord("sys_user_grmember")
gr.addQuery("user", gs.getUserID())
gr.query()
while (gr.next()) {
// Complex logic...
}
})()
// ✅ GOOD - Use conditions when possible
// Condition: gs.getUser().isMemberOf(current.assignment_group)
4. Test ACLs Thoroughly
javascript
// Use "Impersonate User" to test ACLs as different users // Check: Navigation, List views, Forms, Related lists // Verify: Fields hidden, buttons disabled, records filtered
Debug ACLs
Enable ACL Debugging
javascript
// In a background script or temporarily in your code:
gs.setProperty("glide.security.debug", "true")
gs.log("ACL Debug enabled")
// Check System Logs for ACL evaluation details
Check User Permissions
javascript
// Check if current user can read a record
var gr = new GlideRecord("incident")
gr.get("sys_id_here")
gs.info("Can Read: " + gr.canRead())
gs.info("Can Write: " + gr.canWrite())
gs.info("Can Delete: " + gr.canDelete())
// Check field-level
gs.info("Can read assignment_group: " + gr.assignment_group.canRead())
gs.info("Can write assignment_group: " + gr.assignment_group.canWrite())
Common Mistakes
| Mistake | Problem | Solution |
|---|---|---|
| No ACLs on custom tables | Anyone can access | Create ACLs immediately |
| Only role-based ACLs | No row-level security | Add conditions for data segregation |
| Scripts that query DB | Performance issues | Use conditions or cache results |
| Testing only as admin | Admin bypasses ACLs | Test as actual end users |
| Forgetting REST APIs | APIs bypass UI ACLs | Create specific REST ACLs |