ServiceNow CAD study materials
ServiceNow CAD study resources

Vendor Portal Row-Level Security in a Scoped App

ServiceNow CAD · Put your knowledge to work in context.

All case studies

Vendor Portal Row-Level Security in a Scoped App

A vendor portal is built in a scoped app. External vendor users get the role vendor_user via SSO; internal staff get vendor_manager. A read ACL on the vendor table grants vendor_user access, but testing shows vendor_user sees every vendor in the list view. The SSN field is hidden from vendor_user with a UI policy, yet a tester pulled it through a list export. A Script Include in the scoped app needs to look up internal sys_user records, and the team wants to know whether to make it accessible from all scopes. Release is in two weeks.

1. Why does vendor_user see every vendor, and how do you fix it?

Read the worked answer

Because the ACL answers 'may this role read this table?' not 'which rows may this user read?' A role requirement with no condition grants the whole table.

Fix: add a condition to the read ACL that compares the record's vendor company to the user's company, e.g. answer = current.vendor_company == gs.getUser().getRecord().getValue('company');. Keep the role requirement as well; ACLs need both the role/condition to pass.

Things that bite:

  • ACL condition scripts run per row on a list. Cache the user's company once (session property or user attribute) instead of querying inside the condition.
  • Check for a broader ACL that also matches. The most specific matching ACL wins, so a table-level 'any authenticated user' ACL can quietly undercut your row condition.
  • Admins bypass ACLs, so test with a real vendor_user account, not an admin impersonating badly.
  • Add the same row condition to any related table (documents, invoices) - locking the vendor row while leaving child records open leaks the same data.

2. SSN was hidden by a UI policy but exported anyway. Fix it and explain the principle.

Read the worked answer

A UI policy is presentation, not protection. It runs in the browser and anything that does not render a form - list export, REST, background script, import - never sees it.

Fix: add a field-level read ACL on SSN that denies vendor_user (require vendor_manager, or add a condition that fails for external users). Field ACLs are evaluated for list views and exports as well as forms, so the export path closes.

Also worth doing: remove SSN from the default list layout for that role, so it is not merely blocked but not requested.

Principle to state to the team: UI policies shape the experience; ACLs decide access; data policies enforce server-side validity. Any requirement containing the words 'must not see' or 'must not be able to' belongs in an ACL, and any requirement about imports belongs on the server.

3. The Script Include needs to read sys_user. Make it accessible from all scopes, wrap it, or duplicate it?

Read the worked answer

Do not duplicate - a copy of a user-lookup include in a scoped app will drift from the source and quietly return stale data after an upgrade.

Two reasonable options:

  1. Set Accessible from to All application scopes if the API is small, stable, and safe for any app to call. Fastest, but it widens the surface: every scoped app on the instance can now call it, and future signature changes break them.
  2. Global wrapper that exposes two or three narrow methods and calls the real include internally. More artifacts, but the blast radius is bounded and the contract is explicit.

Judgement: if the include returns sensitive user attributes or has broad methods, wrap it. If it is a pure lookup, direct access is fine. Also confirm the scoped app can read sys_user at all - table-level ACLs still apply to the data, so a Script Include being callable does not mean the query succeeds. Test with a vendor_user session before release.