Skip to content

fix(servicenow): add compact role-based DLS to reduce ACL memory - #4392

Merged
Jan-Kazlouski-elastic merged 16 commits into
mainfrom
jan_kazlouski/servicenow-compact-dls
Sep 7, 2026
Merged

Jan-Kazlouski-elastic merged 16 commits into
mainfrom
jan_kazlouski/servicenow-compact-dls

Conversation

@Jan-Kazlouski-elastic

@Jan-Kazlouski-elastic Jan-Kazlouski-elastic commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes https://github.com/elastic/chat-program/issues/49

Part of https://github.com/elastic/chat-program/issues/47.
ServiceNow DLS expands every table read role into individual user_id: / user: / email: entries on each content document (and attachment). For large tenants — especially when a table ACL includes the public role — that can mean hundreds of thousands of ACL entries per document and OOM Elasticsearch during _reindex.

This PR adds compact DLS, opt-in via expand_role_members=false (default true preserves existing behavior). When compact mode is enabled:

  • Content documents store compact role_id:<sys_id> tokens instead of every role member
  • Identity docs include role memberships from sys_user_has_role so DLS term overlap still works
  • Tables with a public read role omit _allow_access_control (world-readable via existing DLS must_not exists)
  • Tables where read roles cannot be resolved omit _allow_access_control (same as public tables; world-readable via existing DLS must_not exists)

Also fixes advanced sync rules to resolve ACL per table instead of stamping an empty ACL on every batch.

Changing the setting requires a full content sync and access control sync.

Checklists

Pre-Review Checklist

  • this PR does NOT contain credentials of any kind, such as API keys or username/passwords (double check config.yml.example)
  • this PR has a meaningful title
  • this PR links to all relevant github issues that it fixes or partially addresses
  • if there is no GH issue, please create it. Each PR should have a link to an issue
  • this PR has a thorough description
  • Covered the changes with automated tests
  • Tested the changes locally
  • Added a label for each target release version (example: v7.13.2, v7.14.0, v8.0.0)
  • For bugfixes: backport safely to all minor branches still receiving patch releases
  • Considered corresponding documentation changes
  • Contributed any configuration settings changes to the configuration reference
  • if you added or changed Rich Configurable Fields for a Native Connector, you made a corresponding PR in Kibana

Changes Requiring Extra Attention

  • Security-related changes (encryption, TLS, SSRF, etc)
  • New external service dependencies added.

Related Pull Requests

Release Note

ServiceNow document-level security can store compact role tokens on content documents instead of expanding every role member onto each record, reducing memory use for large tenants. Disable Expand role members to enable compact mode. The default preserves the previous per-user expansion behavior. Changing the setting requires a full content sync and access control sync.

Store role_id tokens on content documents instead of expanding every
role member onto each record, and enrich identity docs with role
membership. Defaults expand_role_members to false; public tables omit
_allow_access_control. Requires a full content and ACL resync.
elasticmachine and others added 5 commits August 27, 2026 13:54
Omit empty ACL arrays to avoid silent deny-all, preserve legacy expansion
on upgrade when expand_role_members is absent, always enrich identity docs
with roles, and apply table ACLs for advanced-rules syncs when DLS is on.
Jan-Kazlouski-elastic and others added 3 commits August 31, 2026 12:58
Match SharePoint compact DLS: legacy per-user expansion stays the default;
compact role tokens are opt-in via expand_role_members=false.
Distinguish public tables (omit ACL field) from unresolved private tables
(store an empty ACL array to deny access until roles can be resolved).
Comment thread app/connectors_service/connectors/sources/servicenow/datasource.py Outdated
Comment thread app/connectors_service/connectors/sources/servicenow/datasource.py Outdated
Comment thread app/connectors_service/connectors/sources/servicenow/datasource.py Outdated

def _finalize_compact_access_control(self, table_name, role_sys_ids):
compact_acl = list({_prefix_role_id(role_id) for role_id in role_sys_ids})
if not compact_acl:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a table has no read ACL entries, I believe it is public. This will make it inaccessible even when the array is empty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated compact path to omit _allow_access_control when no roles resolve (aligned with your read).
Note: this proposal slightly diverges from legacy’s empty-list behavior. We haven't validated real SN semantics end-to-end yet. We'll try to validate this E2E before merging and keep both legacy and compact modes in best shape possible.

Comment thread app/connectors_service/connectors/sources/servicenow/datasource.py
yield user

async def _fetch_user_roles_map(self):
"""Build a map of user sys_id -> set of role sys_ids from sys_user_has_role."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may OOM. We may want to page this sys_user_has_role table

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd push back on this comment for now.
Fetches are already paginated; we only hold an O(assignments) map (~tens of MB), not per-doc expansion. But I'd be happy to revisit streaming if ACL sync memory shows up in prod.

Comment thread app/connectors_service/connectors/sources/servicenow/datasource.py Outdated
)
return self._finalize_compact_access_control(table_name, role_sys_ids)

roles_by_sys_id = await self._role_sys_id_to_name_map()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if _table_read_role_sys_ids() returns an empty array, we completely ignore this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for your fourth comment - empty compact ACL now returns None (omit field).
Legacy divergence and lack of live SN validation.
Will try to validate during E2E testing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you misunderstand. If role_sys_ids is empty, we never need role_by_sys_id, as the for loop will never execute. It's minor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I misunderstood initially. Pushed the fix now. Should be ok.

Comment thread app/connectors_service/connectors/sources/servicenow/datasource.py Outdated
Guard deleted roles, gate identity role enrichment to compact mode, sort ACL
arrays, cache sys_user_role per sync, treat empty compact ACL as public,
and consolidate role map fetching.
@Jan-Kazlouski-elastic

Copy link
Copy Markdown
Contributor Author

Thank you @erikcurrin-elastic
All of your comments are addressed. What comes to 4 and 8 ones - I'll try to validate the actual SN behavior and make sure both legacy and compact mode work correctly.

@erikcurrin-elastic erikcurrin-elastic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs testing, but the code looks good to me

Fetch table read roles before sys_user_role so empty ACL tables short-circuit
without an unused roles lookup.
Resolve NOTICE.txt conflict by taking main's dependency versions.
@Jan-Kazlouski-elastic

Copy link
Copy Markdown
Contributor Author

E2E validation summary

Tested locally against a ServiceNow developer instance with self-managed connector (elastic-ingest from this branch) + ES/Kibana stack. Connector ArCSe6ABR3Itb2bCjbg9, index content-servicenow-8318.

Compact mode (expand_role_members=false)

  • Full sync: 879 docs indexed; access control sync: 636 identities
  • Content ACLs: role_id: tokens only (0 user_id: tokens)
  • ACL size: 1.8 tokens/doc avg, 5 max (vs 29.3 avg / 68 max in legacy on same data)
  • Total ACL entries: 1,567 (vs 25,717 legacy on same dataset — ~16× fewer)
  • Connector-reported ingest volume: 7 MiB (full sync)
  • Identity docs include role_ids; DLS term overlap verified (content role tokens match identity query params)

Legacy regression (expand_role_members=true)

  • Full sync: 879 docs; ACL sync: 636 identities
  • Content ACLs: user_id: tokens only (0 role_id: tokens)
  • Total ACL entries: 25,717; connector-reported ingest volume: 9 MiB
  • Confirms default/per-user expansion behavior unchanged

Custom table — no read ACLs (u_test_public, label Test Public)

  • Services set to Test Public only; table has empty read ACLs
  • Full sync: 3 docs indexed
  • _allow_access_control omitted on all 3/3 docs (compact mode)
  • Validates the public/no-ACL → omit field path Erik reviewed

Notes

  • Built-in tables (incident, kb_knowledge, etc.) still use hardcoded DEFAULT_SERVICE_NAMES roles in both modes — live SN ACL changes on those tables are not reflected (pre-existing behavior, not introduced here)
  • Unit tests: 60/60 passing in test_servicenow.py

Verdict

E2E looks good — ready to merge for the stated goal (opt-in compact DLS to reduce ACL memory). Legacy default preserved; compact mode and custom-table public/no-ACL behavior validated on a live instance.

@Jan-Kazlouski-elastic

Copy link
Copy Markdown
Contributor Author

E2E validation — compact DLS toggle (ServiceNow)

Environment: local ES 8.x + Kibana stack, dev201762.service-now.com, connector cY7GfKABhShjzDnKILJX, index content-servicenow-f0e9 (services: *, DLS on, 879 docs).

Method: same index throughout; toggle expand_role_members in connector setup + full sync (+ access control sync when switching to compact). No connector worker restart between toggles. Disk comparisons taken after _forcemerge?max_num_segments=1 on both content and ACL indices (raw _cat/indices size immediately post-sync is misleading due to Lucene segment bloat).


Phase summary

Phase Code expand_role_members Content ACL shape ACL entries (879 docs)
1 main absent (legacy default) user_id: only 25,717 (avg 29.3, max 68)
2 PR true user_id: only 25,717 — identical to phase 1
3 PR false (compact) role_id: only 1,567 (avg 1.8, max 5)
4 PR true (toggle back) user_id: only 25,717 — restored

Phases 1↔2 and 3↔4 confirm round-trip on the same index with config toggle + sync only.


Sync performance (full content sync, 879 docs)

Mode Duration Notes
Legacy ~122–128 s phases 1–2
Compact ~17 s phase 3 (re-write after mode switch)
Legacy (re-toggle) ~112 s phase 4

ACS sync: ~7–22 s, 636 identities (unchanged count across modes).


Disk usage (primary store, post-forcemerge)

Index Legacy (phase 4) Compact (phase 3) Δ
Content (879 docs) 1.6 MB 1.4 MB +13%
ACL filter (636 ids) 152 KB 188 KB −19%
Total 1.84 MB 1.65 MB +12%

Compact reduces in-document ACL token volume by ~94% (25,717 → 1,567). On-disk content index is only modestly smaller; ACL index is slightly larger in compact mode due to role enrichment on identity docs.

Note: pre-merge sizes during A/B (e.g. 4.1 MB content right after legacy re-sync) reflect segment/tombstone bloat, not larger documents. Always merge before comparing store size.


Other observations

  • 409 on sync_done: benign optimistic-concurrency conflict when connector config was updated while a job was running; content sync completed, only final connector metadata write failed. Avoid config edits during active syncs / ensure a single elastic-ingest worker.
  • Built-in tables (services: *) still use hardcoded DEFAULT_SERVICE_NAMES for ACLs in both modes — pre-existing, out of scope for this PR.
  • Custom public table (u_test_public): compact omits _allow_access_control (world-readable); legacy writes [] (deny-all). Toggle picked up without worker restart.

Verdict

Ready to merge for the stated goal: opt-in compact DLS (expand_role_members=false) correctly stores role_id: tokens on content docs, resolves membership via ACS, round-trips to legacy on the same index, and dramatically reduces per-document ACL payload with acceptable disk trade-offs at this scale.

Follow-ups (non-blocking): document DEFAULT_SERVICE_NAMES limitation; consider Kibana RCF / release label housekeeping.

@Jan-Kazlouski-elastic Jan-Kazlouski-elastic added bug Something isn't working release_note labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

💔 Failed to create backport PR(s)

Status Branch Result
✅ 9.4 #4460
❌ 8.19 Commit could not be cherrypicked due to conflicts
✅ 9.5 #4461

Successful backport PRs will be merged automatically after passing CI.

To backport manually run:
backport --pr 4392 --autoMerge --autoMergeMethod squash

Jan-Kazlouski-elastic added a commit that referenced this pull request Sep 8, 2026
#4392) (#4460)

Backports the following commits to 9.4:
- fix(servicenow): add compact role-based DLS to reduce ACL memory
(#4392)

Co-authored-by: Jan-Kazlouski-elastic <jan.kazlouski@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Jan-Kazlouski-elastic added a commit that referenced this pull request Sep 8, 2026
#4392) (#4461)

Backports the following commits to 9.5:
- fix(servicenow): add compact role-based DLS to reduce ACL memory
(#4392)

Co-authored-by: Jan-Kazlouski-elastic <jan.kazlouski@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Jan-Kazlouski-elastic added a commit to elastic/kibana that referenced this pull request Sep 8, 2026
## Summary

Adds the `expand_role_members` rich configurable field for the
ServiceNow native connector so Fleet shows the compact role-based DLS
toggle.

Related connectors PR: elastic/connectors#4392

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
- [ ] Review the [backport
guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)
and apply applicable `backport:*` labels.

### Identify risks

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [x] Low risk. This only exposes an existing connector configuration
field in Fleet. No runtime behavior changes in Kibana.

## Release note

ServiceNow connectors now expose an **Expand role members** toggle in
Fleet for compact document-level security.
Jan-Kazlouski-elastic added a commit that referenced this pull request Sep 9, 2026
…ry (#4392) (#4462)

Backports the following commits to 8.19:
- fix(servicenow): add compact role-based DLS to reduce ACL memory
(#4392)
Jan-Kazlouski-elastic added a commit that referenced this pull request Sep 18, 2026
…#4396)

## Closes elastic/chat-program#48

Part of elastic/chat-program#47.
SharePoint Online DLS expands every site group member onto each
document's `_allow_access_control` array. Large site groups can produce
multi-megabyte ACLs per document and OOM Elasticsearch during
`_reindex`.

This PR adds `expand_site_group_members` (default `true` to preserve
existing behavior). When disabled (compact mode):

- Content documents store a compact `site_group:<site_id>:<group_id>`
token instead of every site group member
- Direct user, Entra group, and site user permissions are unchanged on
the document
- ACL sync builds a site-group membership index and enriches identity
docs so DLS term overlap still resolves access (including nested Entra
groups and EEEU guest exclusion)
- If a compact token cannot be written safely (e.g. missing `site_id`),
the connector falls back to expanding site group members and logs a
warning

Changing the setting requires a full content sync and access control
sync.

## Checklists

#### Pre-Review Checklist
- [x] this PR does NOT contain credentials of any kind, such as API keys
or username/passwords (double check `config.yml.example`)
- [x] this PR has a meaningful title
- [x] this PR links to all relevant github issues that it fixes or
partially addresses
- [x] if there is no GH issue, please create it. Each PR should have a
link to an issue
- [x] this PR has a thorough description
- [x] Covered the changes with automated tests
- [x] Tested the changes locally
- [ ] Added a label for each target release version (example: `v7.13.2`,
`v7.14.0`, `v8.0.0`)
- [ ] For bugfixes: backport safely to all minor branches still
receiving patch releases
- [ ] Considered corresponding documentation changes
- [ ] Contributed any configuration settings changes to the
configuration reference
- [x] if you added or changed Rich Configurable Fields for a Native
Connector, you made a corresponding PR in
[Kibana](https://github.com/elastic/kibana/blob/main/packages/kbn-search-connectors/types/native_connectors.ts)

#### Changes Requiring Extra Attention

- [x] Security-related changes (encryption, TLS, SSRF, etc)
- [ ] New external service dependencies added.

## Related Pull Requests

* #4392 — ServiceNow compact
role DLS (same tracking issue)
* elastic/kibana#288070 — Kibana RCF for
`expand_site_group_members`

## Release Note

SharePoint Online document-level security can store compact site-group
tokens on documents instead of expanding every site group member onto
each record, reducing memory use for large site groups. Disable **Expand
site group members** to enable compact mode. The default preserves the
previous behavior. Changing the setting requires a full content sync and
access control sync.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants