Skip to Content
  1. Home
  2. /
  3. Blog
  4. /
  5. ServiceNow Access Control Lists: Advanced Debugging and Scripting Patterns
Monday, April 6, 2026

ServiceNow Access Control Lists: Advanced Debugging and Scripting Patterns

ServiceNow Access Control Lists: Advanced Debugging and Scripting Patterns

Access Control Lists (ACLs) are the gatekeepers of your ServiceNow instance. They determine who can read, write, and execute anything from table records to SOAP messages. Yet most developers treat ACLs as a checkbox — a simple "roles" field set and forgotten. That's a security risk and a maintenance headache waiting to happen.

This guide goes beyond the basics. Whether you're debugging a stubborn "No rights" error or writing an ACL script that needs to handle complex organizational hierarchies, you'll find practical patterns here.

How ServiceNow ACLs Actually Work

Before debugging, understand the evaluation order. ServiceNow evaluates ACLs in this sequence:

  1. Deny ACL — If any deny ACL matches, access is immediately rejected
  2. Allow ACL — If at least one allow ACL matches and no deny ACL fires, access is granted
  3. Default ACL — If no explicit ACL is defined, the system falls back to roles-based access

The critical insight: ACLs short-circuit on deny, not on allow. This means a single deny rule overrides everything. It also means you need to be explicit — ambiguity is not your friend.

ACL Types and Their Evaluation Context

ACL TypeApplies ToEvaluation
ReadTable fields and recordsRow-level and field-level
WriteTable fields and recordsField-level and record creation
DeleteTable recordsRecord-level only
ExecuteScripted REST/SOAP, processorsOperation-level

Debugging ACLs: The Systematic Approach

1. Enable ACL Debugging the Right Way

Don't just toggle ACL debugging globally and wade through a flood of logs. Use scoped debugging instead:

For a specific user:

JavaScript
// Run in background script or Fixit script
gs.debugCILog = 'user_name'; // Replace with actual username

To check a specific record:

JavaScript
// Returns why a user can/cannot access a record
var gr = new GlideRecord('incident');
gr.get('sys_id', 'RECORD_SYS_ID_HERE');
var security = new GlideSecurity(gr);
security.canRead(); // Returns true/false
gs.info('Read allowed: ' + security.canRead());
gs.info('Write allowed: ' + security.canWrite());

2. Use the ACL Debug Log Effectively

Navigate to System Diagnostics > ACL Debug Log. This shows every ACL evaluation for the current session with timestamps and results. Filter by:

  • User — Who triggered the evaluation
  • Table — Which table was evaluated
  • Operation — Read, write, delete, or execute
  • Result — Allow or deny

Pro tip: The debug log is session-bound. You need to reproduce the access issue while the debug log is active. Don't clear it before the test, or you'll lose the evidence.

3. The "No Rights" Error — Common Causes

That generic "No rights" message is unhelpful. Here's how to decode it:

Cause 1: Missing role on the ACL itself Even if a user has all the right table-level roles, the ACL might require a specific role to even evaluate. Check the ACL's "Roles" tab — it lists roles required to invoke the ACL, not roles granted by it.

Cause 2: The ACL script throws an exception If your ACL script has a runtime error, ServiceNow treats it as a deny — silently. Check your script with a try-catch:

JavaScript
(function(acl, gs) {
  try {
    // Your logic here
    var result = someFunction();
    acl.setTitle('Access ' + (result ? 'granted' : 'denied'));
  } catch (e) {
    gs.error('ACL Error: ' + e.getMessage());
    acl.setError('Unexpected error in ACL script');
  }
})(acl, gs);

Cause 3: GlideRecord inside ACL scripts — the silent killer ACLs run on every single record access. A poorly written GlideRecord query inside an ACL script can add hundreds of database calls per page load. This is the most common cause of performance degradation from ACLs.

Advanced ACL Scripting Patterns

Pattern 1: Field-Level Access Based on Record State

Restrict a field to only be editable when the record is in a specific state:

JavaScript
(function(acl, gs) {
  // Get the current record
  var current = acl.getAttribute('sys_object_id');
  
  if (!current) {
    // New record — apply creation rules
    var gr = new GlideRecord('incident');
    if (gr.get(acl.getParameter('sys_unique'))) {
      current = gr;
    }
  }
  
  if (current) {
    var state = current.getValue('state');
    // Allow editing resolved/closed records only to admin-equivalent roles
    if (state == '7' || state == '8') { // Resolved or Closed
      return gs.hasRole('admin') || gs.hasRole('incident_admin');
    }
    // In-progress states — allow assignment group managers
    return gs.hasRole('assignment_manager');
  }
  
  return false;
})(acl, gs);

Pattern 2: Hierarchical Department-Based Access

Grant access based on the user's department matching a record's department, with escalation:

JavaScript
(function(acl, gs) {
  var user = gs.getUser();
  
  // Admins bypass everything
  if (gs.hasRole('admin')) return true;
  
  // Get the target record
  var record = new GlideRecord('ast_cloud_server');
  if (!record.get(acl.getParameter('sys_unique'))) return false;
  
  // Get user's department
  var userDept = user.getDepartmentID();
  var recordDept = record.getValue('u_department');
  
  // Direct match — allow
  if (userDept == recordDept) return true;
  
  // Check if user's department is a parent of record's department
  var deptGr = new GlideRecord('cmn_department');
  if (deptGr.get(recordDept)) {
    var parent = deptGr.getValue('parent');
    while (parent) {
      if (parent == userDept) return true;
      if (deptGr.get(parent)) {
        parent = deptGr.getValue('parent');
      } else {
        break;
      }
    }
  }
  
  // Check delegation — user might be acting on behalf of someone in another dept
  var delegateGr = new GlideRecord('sys_user_delegate');
  delegateGr.addQuery('user', user.getUserID());
  delegateGr.addQuery('started', '<=', gs.nowDateTime);
  delegateGr.addQuery('expires', '>=', gs.nowDateTime);
  delegateGr.query();
  if (delegateGr.next()) {
    var delegateUser = delegateGr.getValue('delegate');
    var delegateDeptGr = new GlideRecord('sys_user');
    if (delegateDeptGr.get(delegateUser)) {
      if (delegateDeptGr.getValue('department') == recordDept) return true;
    }
  }
  
  return false;
})(acl, gs);

Note: Deep hierarchy traversal with GlideRecord can be expensive. Cache department relationships in a Script Include if you have many records to evaluate.

Pattern 3: Time-Based ACL Restrictions

Allow access to sensitive fields only during business hours:

JavaScript
(function(acl, gs) {
  // Allow admins always
  if (gs.hasRole('admin')) return true;
  
  var now = new GlideDateTime();
  var dayOfWeek = now.getDayOfWeekLocalTime(); // 1 = Sunday, 7 = Saturday
  
  // Weekend — deny unless admin
  if (dayOfWeek == 1 || dayOfWeek == 7) {
    acl.setError('Changes not allowed on weekends');
    return false;
  }
  
  var hour = now.getHourLocalTime();
  
  // Outside business hours (9am–6pm)
  if (hour < 9 || hour >= 18) {
    acl.setError('Changes allowed only between 9 AM and 6 PM');
    return false;
  }
  
  return true;
})(acl, gs);

Pattern 4: Execute ACLs for Scripted REST APIs

Secure your scripted REST APIs with execute ACLs that validate more than just roles:

JavaScript
(function(acl, gs) {
  var user = gs.getUser();
  
  // Must be authenticated
  if (!user.isLoggedIn()) {
    acl.setError('Authentication required');
    return false;
  }
  
  // Check for specific role AND IP range
  var ip = gs.getSession().getAttribute('originalIPAddress');
  var allowedPrefix = '10.0.'; // Internal network range
  
  if (gs.hasRole('rest_api_client')) {
    // Validate IP only for external clients
    if (ip && ip.indexOf(allowedPrefix) !== 0) {
      // Log the external access attempt
      gs.eventQueue('unauthorized_rest_access', {
        user: user.getUserID(),
        ip: ip,
        time: gs.nowDateTime
      });
      acl.setError('REST API access restricted to internal network');
      return false;
    }
    return true;
  }
  
  return false;
})(acl, gs);

ACL Performance: The Hidden Performance Killer

ACLs are evaluated on every field, on every record, for every user. A slow ACL script multiplies rapidly. Here's how to keep ACLs fast:

Rule 1: Never query the database in a field-level ACL

Field-level ACLs run per field per record. A single query multiplied across dozens of fields and hundreds of records is a disaster. Instead:

JavaScript
// BAD — database query in field ACL
(function(acl, gs) {
  var gr = new GlideRecord('u_approval_config');
  gr.addQuery('u_table_name', 'incident');
  gr.addQuery('u_field_name', 'priority');
  gr.query();
  if (gr.next()) {
    return gs.hasRole(gr.getValue('u_required_role'));
  }
  return false;
})(acl, gs);

// BETTER — use ACL attributes to store configuration
// Set attribute on the field: u_required_role=itil_admin
// ACL script reads it without a database call:
(function(acl, gs) {
  var requiredRole = acl.getAttribute('u_required_role');
  if (!requiredRole) return false;
  return gs.hasRole(requiredRole);
})(acl, gs);

Rule 2: Use GlideSystem (gs) methods over GlideRecord where possible

gs.hasRole() is evaluated in memory — it's fast. new GlideRecord().query() hits the database every time.

Rule 3: Cache expensive computations

JavaScript
// In a Script Include — cache hierarchical data
var DepartmentCache = Class.create();
DepartmentCache.prototype = {
  cache: {},
  
  initialize: function() {
    // Pre-warm cache on first use
  },
  
  isInHierarchy: function(userDeptId, recordDeptId) {
    var key = userDeptId + ':' + recordDeptId;
    if (this.cache[key] !== undefined) {
      return this.cache[key];
    }
    
    // Traverse hierarchy
    var result = this._checkParent(userDeptId, recordDeptId);
    this.cache[key] = result;
    return result;
  },
  
  _checkParent: function(current, target) {
    // Implementation here
    return false; // Placeholder
  },
  
  type: 'DepartmentCache'
};

Common ACL Mistakes to Avoid

1. Forgetting the "Other" ACL tab Each ACL has Read, Write, Delete, and Execute tabs. For table access, the table-level ACL is in the "Read" tab. But field-level ACLs live on the field itself. Don't confuse the two.

2. Assuming ACL scripts run with elevated privileges By default, ACL scripts run with the requesting user's privileges. Use gs.getSession().elevatePrivilege() only when absolutely necessary — and document why.

3. Not testing with the actual user, not your admin account Always test ACL behavior from a non-privileged user account. Your admin account bypasses most ACLs, making it useless for testing.

4. Overly permissive "allow all" ACLs Setting return true; for all writes on a table because "the UI handles it" is a classic mistake. UI actions still go through ACLs server-side.

Quick ACL Debugging Checklist

When an access issue surfaces, work through this:

  • Does the user have the role listed in the ACL's "Roles" tab?
  • Is the ACL active (not skipped)?
  • Does the ACL script throw an exception silently?
  • Is there a deny ACL firing before the allow ACL?
  • Are there inheritance issues with field ACLs on extended tables?
  • Is this a scoped app ACL vs. global ACL conflict?
  • Does the user meet the condition expression (not just the script)?

FAQ

Q: Why does my ACL pass in the ACL test but fail in production? The ACL test page runs as the System Import Set user or your admin context. Always test as the actual end user who is experiencing the issue.

Q: Can an ACL return neither true nor false? Yes — if no explicit ACL matches, the system falls through to the default roles-based behavior. This can cause confusing results where the ACL "passes" the test but fails in real usage.

Q: How do ACLs interact with ACL rules on extended tables? Child table ACLs are evaluated first. If no matching ACL is found, the system walks up the inheritance chain to parent tables. A deny on a parent table cannot be overridden by an allow on a child table.

Q: Do ACLs apply to Web Services? Yes. SOAP, REST, and even internal GlideRecord operations all enforce ACLs server-side. This is a common misconception — many assume Web Services bypass ACLs because they don't go through the UI.

Conclusion

ACLs are powerful and consequential. A well-written ACL script protects sensitive data precisely; a sloppy one either leaks data or locks out legitimate users. The patterns in this guide — especially the no-database-query rule for field ACLs and the systematic debugging approach — will save you hours of frustration and keep your instance secure.

Start auditing your ACL scripts today. Pay particular attention to any that contain new GlideRecord — those are your biggest performance and correctness risks.


Need help with a specific ACL scenario? The SN-Tricks Scripts Library has ready-to-use patterns for common access control patterns. Check it out.

Was this article helpful?