Skip to content

Require UPDATE_SCHEMA permission on all schema mutators#4423

Merged
lvca merged 1 commit into
mainfrom
security/schema-mutation-authorization
May 30, 2026
Merged

Require UPDATE_SCHEMA permission on all schema mutators#4423
lvca merged 1 commit into
mainfrom
security/schema-mutation-authorization

Conversation

@lvca
Copy link
Copy Markdown
Member

@lvca lvca commented May 30, 2026

Summary

Extends the UPDATE_SCHEMA authorization check (previously applied only to createProperty) to every public schema-mutating method, closing an incorrect-authorization gap where a non-administrator identity could still alter the database schema.

  • LocalDocumentType - added a shared checkForSchemaMutation() helper and gated: rename, dropProperty, addSuperType (chokepoint covering all overloads), removeSuperType (chokepoint), setSuperTypes, setAliases, addBucket, removeBucket. The existing createProperty check now uses the same helper.
  • LocalProperty - gated all 12 setters: setDefaultValue, setOfType, setReadonly, setMandatory, setNotNull, setHidden, setExternal, setCompression, setMax, setMin, setRegexp, setCustomValue.

Safety of internal paths

The check is a no-op in embedded mode and in system contexts with no bound user. Schema reload (DocumentType.createProperty(String, JSONObject)) calls the already-gated createProperty plus every LocalProperty setter, and has shipped that way since 26.4.2 without breaking reload - so reload and HA replication apply run in a no-current-user context where the new checks are no-ops. dropType and TypeBuilder already check UPDATE_SCHEMA at entry, so their internal mutator calls are harmless redundant re-checks.

Tests

  • New SchemaMutationAuthorizationIT (server): a read-only API token gets HTTP 403 on DROP PROPERTY, ALTER TYPE SUPERTYPE +/-, ALTER TYPE NAME, and ALTER PROPERTY MANDATORY, while an administrator still succeeds (positive control).
  • Regression-checked: 159 embedded engine schema tests, CrossDatabaseAccessIT, HA RaftReplicationChangeSchemaIT (replication intact), OpenCypher label tests.

Extends the authorization check previously applied to createProperty to
every public schema-mutating method, closing an incorrect-authorization
gap where a non-admin identity could still alter the schema.

- LocalDocumentType: rename, dropProperty, addSuperType, removeSuperType,
  setSuperTypes, setAliases, addBucket, removeBucket now check UPDATE_SCHEMA
  via a shared checkForSchemaMutation() helper.
- LocalProperty: setDefaultValue, setOfType, setReadonly, setMandatory,
  setNotNull, setHidden, setExternal, setCompression, setMax, setMin,
  setRegexp, setCustomValue now check UPDATE_SCHEMA.

The check is a no-op in embedded mode and in system contexts with no bound
user (schema load at startup, HA replication apply), so internal paths are
unaffected. Adds a server integration regression test.
@lvca lvca added this to the 26.6.1 milestone May 30, 2026
@lvca lvca merged commit 2ee76fe into main May 30, 2026
13 of 15 checks passed
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces centralized schema mutation authorization checks (checkForSchemaMutation()) in LocalDocumentType and LocalProperty to enforce the UPDATE_SCHEMA permission, addressing an incomplete fix for schema mutation access. It also adds a regression integration test to verify these restrictions. The reviewer feedback points out that some schema-mutating methods in LocalDocumentType (setBucketSelectionStrategy and setCustomValue) are still missing these checks, and suggests expanding the integration tests to cover them.

Comment on lines +136 to +138
protected void checkForSchemaMutation() {
((DatabaseInternal) schema.getDatabase()).checkPermissionsOnDatabase(SecurityDatabaseUser.DATABASE_ACCESS.UPDATE_SCHEMA);
}
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.

security-high high

While checkForSchemaMutation() is a great addition to centralize permission checks, there are other public schema-mutating methods in LocalDocumentType that are currently missing this check:

  1. setBucketSelectionStrategy(final BucketSelectionStrategy selectionStrategy) (and its overload)
  2. setCustomValue(final String key, final Object value)

Without gating these methods, a non-administrator identity could still alter the database schema's bucket selection strategy or custom metadata. Please add checkForSchemaMutation(); at the beginning of these methods as well.

Comment on lines +53 to +62
assertThat(command(serverIndex, token, "DROP PROPERTY Memory.salience"))
.as("read-only token must not DROP PROPERTY").isEqualTo(403);
assertThat(command(serverIndex, token, "ALTER TYPE Memory SUPERTYPE +Persisted"))
.as("read-only token must not add a SUPERTYPE").isEqualTo(403);
assertThat(command(serverIndex, token, "ALTER TYPE Memory SUPERTYPE -Persisted"))
.as("read-only token must not remove a SUPERTYPE").isEqualTo(403);
assertThat(command(serverIndex, token, "ALTER TYPE Memory NAME MemoryRenamed"))
.as("read-only token must not rename a type").isEqualTo(403);
assertThat(command(serverIndex, token, "ALTER PROPERTY Memory.salience MANDATORY true"))
.as("read-only token must not alter a property constraint").isEqualTo(403);
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.

medium

Please add test cases for ALTER TYPE ... BUCKETSELECTIONSTRATEGY and ALTER TYPE ... CUSTOM to ensure complete coverage of all schema mutators once those methods are gated with checkForSchemaMutation() in LocalDocumentType.

        assertThat(command(serverIndex, token, "DROP PROPERTY Memory.salience"))
            .as("read-only token must not DROP PROPERTY").isEqualTo(403);
        assertThat(command(serverIndex, token, "ALTER TYPE Memory SUPERTYPE +Persisted"))
            .as("read-only token must not add a SUPERTYPE").isEqualTo(403);
        assertThat(command(serverIndex, token, "ALTER TYPE Memory SUPERTYPE -Persisted"))
            .as("read-only token must not remove a SUPERTYPE").isEqualTo(403);
        assertThat(command(serverIndex, token, "ALTER TYPE Memory NAME MemoryRenamed"))
            .as("read-only token must not rename a type").isEqualTo(403);
        assertThat(command(serverIndex, token, "ALTER PROPERTY Memory.salience MANDATORY true"))
            .as("read-only token must not alter a property constraint").isEqualTo(403);
        assertThat(command(serverIndex, token, "ALTER TYPE Memory BUCKETSELECTIONSTRATEGY partitioned(salience)"))
            .as("read-only token must not alter bucket selection strategy").isEqualTo(403);
        assertThat(command(serverIndex, token, "ALTER TYPE Memory CUSTOM description='test'"))
            .as("read-only token must not alter custom metadata").isEqualTo(403);

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 30, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 6 complexity

Metric Results
Complexity 6

View in Codacy

🟢 Coverage 100.00% diff coverage

Metric Results
Coverage variation Report missing for bdc414c1
Diff coverage 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (bdc414c) Report Missing Report Missing Report Missing
Head commit (fb7755a) 159976 105856 66.17%

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#4423) 26 26 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@lvca lvca self-assigned this May 30, 2026
@lvca lvca deleted the security/schema-mutation-authorization branch May 30, 2026 14:44
@claude
Copy link
Copy Markdown

claude Bot commented May 30, 2026


Code Review

Overview

This PR closes a real authorization gap: createProperty was the only schema mutator guarded by UPDATE_SCHEMA, leaving rename, dropProperty, addSuperType, removeSuperType, setSuperTypes, setAliases, addBucket, removeBucket, and all 12 LocalProperty setters reachable by a read-only identity. The fix is correct and well-scoped.


Strengths

  • Clean helper abstraction. Both checkForSchemaMutation() helpers are one-liners that call the existing checkPermissionsOnDatabase path. No new permission logic is introduced - good reuse.
  • Chokepoint placement. Gating the package-private addSuperType(DocumentType, boolean) at line 1614 covers all three public overloads without any change to their bodies. The overload chain is easy to follow.
  • Consistent with LocalProperty. The helper in LocalProperty uses owner.getSchema().getEmbedded().getDatabase(), which is identical to the pattern used by every other setter in that class. No inconsistency introduced.
  • No-op in embedded/system contexts. The underlying checkPermissionsOnDatabase (LocalDatabase:693) short-circuits when security == null, no context exists, or no current user is bound - so schema reload, HA replication apply, and embedded mode are unaffected.
  • Tests with positive controls. The IT validates that a read-only token is blocked AND that an admin still succeeds - both halves matter.

Issues / Suggestions

1. Redundant permission checks in setSuperTypes (minor)

setSuperTypes (line 294) calls checkForSchemaMutation() at its top, then delegates to toRemove.forEach(this::removeSuperType) and toAdd.forEach(this::addSuperType). Both removeSuperType (line 174) and addSuperType(DocumentType, boolean) (line 1614) also call checkForSchemaMutation(). For a batch of N super-type changes this means N+1 permission lookups.

This is currently harmless - the check is a cheap thread-local lookup and batch sizes are always tiny. But the PR description claims the two delegates are the chokepoints while setSuperTypes itself should be the entry-point check only. The current code has both, which is slightly inconsistent. Consider a brief comment explaining the intent (fail-fast at the top, direct-caller safety at each delegate).

2. checkForSchemaMutation() visibility in LocalDocumentType (minor)

The method is declared protected. Since no subclass in the tree needs to call it directly and the intent is purely internal enforcement, private would be the safer default. If a future subclass needs it, visibility can be widened then.

3. Test does not clean up types between test methods (low risk)

readOnlyTokenCannotMutateSchema creates Memory and Persisted; adminCanStillMutateSchema creates Doc and Base. Neither test drops those types. The base class wipes the database folder at suite teardown, so isolation across runs is fine. However if testEachServer re-uses the same database across methods within a single run the second method could fail with a "type already exists" error if names ever collide. The names are currently distinct - but adding teardown DROP TYPE calls or using a random name suffix would make the test more robust.

4. Test covers only one of the 12 LocalProperty setters

Only ALTER PROPERTY Memory.salience MANDATORY true is exercised. Since all 12 setters call the same checkForSchemaMutation() helper, this is a reasonable proxy. A comment like // representative of all 12 LocalProperty setters would make the intent clear to a future reader.

5. CVE reference in test Javadoc

The class-level Javadoc cites "CVE-2026-44221" (a year-2026 identifier). If this is an internal tracking ID rather than a published CVE, removing the CVE prefix or adding a clarifying note would avoid confusion.

6. ALTER TYPE Memory SUPERTYPE -Persisted tests removal of a never-added supertype

In readOnlyTokenCannotMutateSchema, the +Persisted attempt returns 403 and is never applied, so Persisted is not actually a supertype of Memory when the -Persisted removal runs. The authorization check fires before ArcadeDB validates whether the supertype exists (confirmed by the order in removeSuperType: checkForSchemaMutation() before recordFileChanges), so the 403 is still returned. This is correct - just worth a comment noting the test relies on auth-check-first ordering, so a future reorder would not silently break test semantics.


Summary

The security fix is correct, minimal, and well-tested. The items above are all minor quality notes - none block merging. The most actionable one is tightening checkForSchemaMutation() to private in LocalDocumentType, which requires no behavioral change.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.49%. Comparing base (9945018) to head (fb7755a).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4423      +/-   ##
============================================
+ Coverage     64.45%   64.49%   +0.03%     
  Complexity      431      431              
============================================
  Files          1648     1649       +1     
  Lines        128194   128270      +76     
  Branches      27477    27507      +30     
============================================
+ Hits          82630    82729      +99     
+ Misses        33948    33927      -21     
+ Partials      11616    11614       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant