UNPKG

@servicenow/sdk

Version:
285 lines (222 loc) 9.41 kB
--- tags: [security, acl, access, access-control, role, permissions, data-filter, security-attribute, sys_security_acl, sys_user_role] --- # 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 | 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 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`), look up the `sys_id` from `sys_user_role`. 4. You cannot rename roles after they are saved. ### Role API Reference For the full property reference, see the `role-api` topic. ### Role Example ```javascript 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 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). ### ACL API Reference For the full property reference, see the `acl-api` topic. ### ACL Examples **Table-level ACL with roles:** ```javascript 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:** ```javascript 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):** ```javascript 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. ```javascript // 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. ```javascript 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 `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 ```typescript fluent 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 | 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_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 ```typescript fluent 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