Skip to content

support ALTER DEFAULT PRIVILEGES - #2810

Open
jennifersp wants to merge 10 commits into
mainfrom
jennifer/default-privs
Open

jennifersp wants to merge 10 commits into
mainfrom
jennifer/default-privs

Conversation

@jennifersp

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

github-actions Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

@jennifersp DOLT

read_tests from_latency to_latency percent_change
covering_index_scan_postgres 2.48 2.48 0.0
groupby_scan_postgres 84.47 82.96 -1.79
index_join_postgres 2.35 2.39 1.7
index_join_scan_postgres 1.7 1.7 0.0
index_scan_postgres 467.3 467.3 0.0
oltp_point_select 0.39 0.39 0.0
oltp_read_only 6.67 6.67 0.0
select_random_points 0.74 0.74 0.0
select_random_ranges 1.12 1.1 -1.79
table_scan_postgres 467.3 467.3 0.0
types_table_scan_postgres 1213.57 1191.92 -1.78
write_tests from_latency to_latency percent_change
oltp_delete_insert_postgres 4.57 4.49 -1.75
oltp_insert 2.3 2.26 -1.74
oltp_read_write 12.75 12.75 0.0
oltp_update_index 2.48 2.48 0.0
oltp_update_non_index 2.22 2.18 -1.8
oltp_write_only 5.88 5.88 0.0
types_delete_insert_postgres 5.0 5.0 0.0

@github-actions

github-actions Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor
Main PR
Total 42090 42090
Successful 20955 20963
Failures 21135 21127
Partial Successes1 5405 5405
Main PR
Successful 49.7862% 49.8052%
Failures 50.2138% 50.1948%

${\color{lightgreen}Progressions (8)}$

dependency

QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_dep_user1 IN SCHEMA deptest
  GRANT ALL ON TABLES TO regress_dep_user2;

event_trigger

QUERY: alter default privileges for role regress_evt_user
 revoke delete on tables from regress_evt_user;

matview

QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_matview_user
  REVOKE INSERT ON TABLES FROM regress_matview_user;
QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_matview_user
  GRANT INSERT ON TABLES TO regress_matview_user;

object_address

QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;

select_into

QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_selinto_user
	  REVOKE INSERT ON TABLES FROM regress_selinto_user;
QUERY: ALTER DEFAULT PRIVILEGES FOR ROLE regress_selinto_user
	  GRANT INSERT ON TABLES TO regress_selinto_user;

Footnotes

  1. These are tests that we're marking as Successful, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct. ↩

@itoqa

itoqa Bot commented Jun 4, 2026

Copy link
Copy Markdown

Ito Test Report ❌

17 test cases ran. 3 failed, 3 additional findings, 11 passed.

Across 17 test cases, 11 passed and 6 failed: core default-privilege inheritance behaviors were mostly validated end-to-end (including tables, wildcard schemas, sequences, routines, a race scenario, and a non-enumerating unauthorized error path), and one previously unstable privilege-inheritance path now executes successfully in a stabilized environment. The most important confirmed defects are a critical startup crash on malformed auth.db deserialization, a high-severity legacy v1 auth.db role-ID collision issue that can break role lookups, and four medium-severity logic flaws (ALTER DEFAULT PRIVILEGES without FOR ROLE/USER being wrongly denied for non-superusers, in-memory/on-disk ACL drift when persistence fails, CREATE TABLE default ACL persistence asymmetry versus CREATE SEQUENCE, and ALTER INDEX ATTACH PARTITION parsing but always failing due to hardcoded unsupported execution).

❌ Failed (3)
Category Summary Screenshot
Creation 🟠 CREATE TABLE objects did not retain expected inherited table ACLs while CREATE SEQUENCE objects retained inherited sequence ACLs. CREATION-6
Privileges 🟠 ALTER DEFAULT PRIVILEGES without FOR ROLE/USER is denied for non-superusers due to empty auth target handling. PRIVILEGES-5
Privileges 🟠 Default ACL state mutates before persistence and can drift from disk when persistence fails. PRIVILEGES-6
🟠 Default table privileges are not persisted during CREATE TABLE flow
  • What failed: New sequences retained inherited access, but new tables did not inherit/persist expected default SELECT access for readonly_user; both object types should apply and persist their configured defaults.
  • Impact: Teams depending on default table ACL inheritance can silently lose expected access on newly created tables, requiring manual grants. This creates inconsistent permission behavior between object types in the same workflow.
  • Steps to reproduce:
    1. Configure default SELECT on tables and default USAGE on sequences for the same owner and schema.
    2. Create multiple tables and sequences in parallel as that owner.
    3. Restart the server and compare inherited grantee access on the new tables versus new sequences.
  • Stub / mock context: Authentication was bypassed via server/authentication_scram.go to run deterministic seeded-role SQL checks, and local auth bootstrap state was repaired before executing this scenario. No route interception or API response stubbing was used; the defect finding comes from direct source-code inspection of the production create paths.
  • Code analysis: I inspected default-privilege application paths for table and sequence creation. The table path defers applying defaults until the iterator returns io.EOF, while the sequence path applies defaults and persists changes directly in RowIter, creating an asymmetric and brittle persistence point for tables.
  • Why this is likely a bug: The table ACL apply/persist hook is coupled to iterator EOF instead of the successful create path, so table defaults can be skipped while equivalent sequence defaults are persisted immediately.

Relevant code:

server/node/create_table.go (lines 120-133)

func (i *createTableDefaultPrivsIter) Next(ctx *sql.Context) (sql.Row, error) {
    row, err := i.inner.Next(ctx)
    if err == io.EOF && !i.applied {
        i.applied = true
        var applyErr error
        auth.LockWrite(func() {
            auth.ApplyDefaultPrivilegesForNewTable(i.ownerID, i.schemaName, i.tableName)
            applyErr = auth.PersistChanges()
        })
        if applyErr != nil {
            return nil, applyErr
        }
    }
    return row, err
}

server/node/create_sequence.go (lines 229-236)

var authErr error
auth.LockWrite(func() {
    ownerRole := auth.GetRole(ctx.Client().User)
    if ownerRole.IsValid() {
        auth.ApplyDefaultPrivilegesForNewSequence(ownerRole.ID(), c.sequence.Id.SchemaName(), c.sequence.Id.SequenceName())
    }
    authErr = auth.PersistChanges()
})

server/auth/default_privileges.go (lines 135-156)

// ApplyDefaultPrivilegesForNewTable applies any matching default privileges to a newly created table.
// Must be called under LockWrite.
func ApplyDefaultPrivilegesForNewTable(ownerRoleID RoleID, schemaName, tableName string) {
    for key, dpv := range globalDatabase.defaultPrivileges.Data {
        if key.OwnerRole != ownerRoleID || key.ObjectType != PrivilegeObject_TABLE {
            continue
        }
        if key.Schema != "" && key.Schema != schemaName {
            continue
        }
        for granteeID, granteeValue := range dpv.Grantees {
            for _, privilegeMap := range granteeValue.Privileges {
                for grantedPriv, withGrantOption := range privilegeMap {
🟠 No-FOR-ROLE statement denied before owner fallback resolution
  • What failed: The command is denied with a permission error for an empty target instead of treating the session user as owner; expected behavior is self-context authorization without requiring explicit FOR ROLE/USER.
  • Impact: Non-superusers cannot use a valid ALTER DEFAULT PRIVILEGES form unless they add an explicit owner clause. This breaks expected SQL compatibility for a meaningful privilege-management workflow.
  • Steps to reproduce:
    1. Connect as a non-superuser role in a clean session.
    2. Run ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_user without a FOR ROLE/USER clause.
    3. Create a table as that role and verify readonly_user still lacks inherited SELECT privileges.
  • Stub / mock context: Authentication was bypassed by disabling SCRAM checks in server/authentication_scram.go so local SQL role behavior could be tested without external auth dependencies. Local roles and schema permissions were seeded before the authorization scenario was executed.
  • Code analysis: I inspected AST conversion, auth gate checks, and execution fallback in server/ast/alter_default_privileges.go, server/auth/auth_handler.go, and server/node/alter_default_privileges.go. The auth metadata always includes one target name even when empty, and authorization rejects that empty target before executor fallback can resolve the current user.
  • Why this is likely a bug: Production code proves a valid empty-owner form is rejected in auth before the built-in current-user fallback runs, so this is a logic-order defect rather than test setup noise.

Relevant code:

server/ast/alter_default_privileges.go (lines 43-50)

return vitess.InjectedStatement{
	Auth: vitess.AuthInformation{
		AuthType:    auth.AuthType_CREATE,
		TargetType:  auth.AuthTargetType_AlterDefaultPrivilegesIdentifiers,
		TargetNames: []string{node.TargetRole},
	},
	Statement: &pgnodes.AlterDefaultPrivileges{

server/auth/auth_handler.go (lines 224-235)

case AuthTargetType_AlterDefaultPrivilegesIdentifiers:
	nTargets := len(auth.TargetNames)
	if nTargets > 1 {
		return errors.Errorf("function identifiers has an unsupported count: %d", len(auth.TargetNames))
	}
	if state.role.IsSuperUser {
		return nil
	}
	if nTargets == 1 && state.role.Name == auth.TargetNames[0] {
		return nil
	}
	return errors.Errorf("permission denied for %s", auth.TargetNames[0])

server/node/alter_default_privileges.go (lines 147-155)

func (n *AlterDefaultPrivileges) resolveOwnerRole(ctx *sql.Context) (auth.Role, error) {
	// empty means current user
	if n.OwnerRole == "" {
		userRole := auth.GetRole(ctx.Client().User)
		if !userRole.IsValid() {
			return auth.Role{}, errors.Errorf(`role "%s" does not exist`, ctx.Client().User)
		}
		return userRole, nil
🟠 Persist error after ALTER leaves in-memory and on-disk ACL drift
  • What failed: In-memory default privileges are updated before persistence, and on persistence error there is no rollback; expected behavior is atomic failure semantics with no durable/in-memory drift.
  • Impact: Operators can observe conflicting privilege behavior before and after restart after a failed ALTER write. This can cause inconsistent access control outcomes during incident recovery.
  • Steps to reproduce:
    1. Induce an auth.db write failure while the process remains running.
    2. Execute ALTER DEFAULT PRIVILEGES and capture the returned persistence outcome.
    3. Compare effective default privilege behavior before restart and after restart to detect memory-disk divergence.
  • Stub / mock context: Authentication was bypassed by disabling SCRAM checks in server/authentication_scram.go to keep role/ACL verification deterministic in local execution. The run also simulated auth persistence failures by changing filesystem write behavior, including redirecting the auth database path to a full device.
  • Code analysis: I inspected RowIter execution ordering in server/node/alter_default_privileges.go, write behavior in server/auth/serialization.go, and default privilege mutation in server/auth/default_privileges.go. The node executes mutations first, then persists, and mutation helpers directly write global in-memory state.
  • Why this is likely a bug: The production write path has no transaction or rollback around mutable global ACL state, so persistence failure can deterministically leave memory and disk out of sync.

Relevant code:

server/node/alter_default_privileges.go (lines 63-70)

var err error
auth.LockWrite(func() {
	err = n.execute(ctx)
	if err != nil {
		return
	}
	err = auth.PersistChanges()
})

server/auth/serialization.go (lines 25-28)

func PersistChanges() error {
	if fileSystem != nil {
		return fileSystem.WriteFile(authFileName, globalDatabase.serialize(), 0644)
	}

server/auth/default_privileges.go (lines 53-77)

// AddDefaultPrivilege adds a default privilege entry to the global database.
func AddDefaultPrivilege(key DefaultPrivilegeKey, grantee RoleID, privilege GrantedPrivilege, withGrantOption bool) {
	dpv, ok := globalDatabase.defaultPrivileges.Data[key]
	if !ok {
		dpv = DefaultPrivilegeValue{
			Key:      key,
			Grantees: make(map[RoleID]DefaultPrivilegeGranteeValue),
		}
	}
	granteeValue, ok := dpv.Grantees[grantee]
	if !ok {
		granteeValue = DefaultPrivilegeGranteeValue{
			Grantee:    grantee,
			Privileges: make(map[Privilege]map[GrantedPrivilege]bool),
		}
	}
	privilegeMap, ok := granteeValue.Privileges[privilege.Privilege]
	if !ok {
✅ Passed (11)
Category Summary Screenshot
Creation New public table inherited configured default SELECT privileges for readonly_user. CREATION-1
Creation Schema-wildcard default table privileges applied to both public and alt schema tables. CREATION-2
Creation New sequence inherited configured USAGE privileges for seq_reader and nextval succeeded. CREATION-3
Creation ALTER DEFAULT PRIVILEGES for routines applied as expected; func_reader executed both newly created routine objects. CREATION-4
Creation Concurrent ALTER DEFAULT PRIVILEGES and CREATE TABLE operations produced consistent ACL outcomes before and after restart in this run. CREATION-5
Parser Single-owner FOR ROLE syntax parsed and executed successfully, and inherited SELECT access worked on a newly created table. PARSER-1
Parser ALTER TABLE ATTACH PARTITION and DETACH PARTITION both executed successfully using the intended partition identifier. PARSER-3
Parser Legacy multi-owner FOR USER syntax was rejected with a controlled syntax error, which matches expected behavior. PARSER-4
Privileges Re-executed successfully in a stabilized environment: ALTER DEFAULT PRIVILEGES FOR USER auth_test_super ... GRANT SELECT ON TABLES TO readonly_user succeeded, and readonly_user could SELECT from newly created another_table without manual GRANT. PRIVILEGES-1
Privileges Unauthorized ALTER checks stop at permission denial before role existence lookup. PRIVILEGES-7
Serialization Default privileges persisted across restart and were still enforced for readonly access after reload. SERIALIZATION-1
ℹ️ Additional Findings (3)

These findings are unrelated to the current changes but were observed during testing.

Category Summary Screenshot
Parser 🟠 ALTER INDEX ATTACH PARTITION parses, but execution is hard-blocked by an unconditional unsupported error in production conversion logic. PARSER-2
Serialization ⚠️ Legacy v1 auth.db startup can degrade auth state and trigger repeated role public does not exist failures. SERIALIZATION-2
Serialization 🚨 Truncated auth.db bytes can crash startup with an unhandled panic instead of a controlled error. SERIALIZATION-3
🟠 ALTER INDEX ATTACH PARTITION cannot execute
  • What failed: The statement shape is accepted by parser/AST construction, but execution always fails before planning because ALTER INDEX conversion is hardcoded to return NotYetSupportedError; expected behavior is successful execution for supported ATTACH PARTITION flows.
  • Impact: Users cannot complete a meaningful partition-management workflow with ALTER INDEX ATTACH PARTITION even when SQL parses correctly. This blocks expected DDL behavior without a direct in-product workaround.
  • Steps to reproduce:
    1. Create parent and child index objects for a partition attach flow.
    2. Run ALTER INDEX public.parent_idx ATTACH PARTITION public.child_idx.
    3. Observe that execution returns unsupported instead of applying the attach operation.
  • Stub / mock context: Real SCRAM sign-in was bypassed to keep local SQL sessions deterministic in this run; no API response mocking or route interception was applied for this test.
  • Code analysis: I verified parser and conversion paths for ALTER INDEX in the repo: parser grammar builds an AlterIndexAttachPartition node, conversion dispatch reaches nodeAlterIndex, and that converter unconditionally returns an unsupported error for all ALTER INDEX statements.
  • Why this is likely a bug: Production code accepts this statement shape in parser/dispatch but then always rejects execution, indicating missing implementation in a live code path rather than test setup noise.

Relevant code:

postgres/parser/parser/sql.y (lines 2029-2032)

| ALTER INDEX table_index_name ATTACH PARTITION db_object_name
  {
    $$.val = &tree.AlterIndex{Index: $3.tableIndexName(), Cmd: &tree.AlterIndexAttachPartition{Index: $6.unresolvedObjectName()}}
  }

server/ast/convert.go (lines 46-47)

case *tree.AlterIndex:
		return nodeAlterIndex(ctx, stmt)

server/ast/alter_index.go (lines 24-30)

func nodeAlterIndex(ctx *Context, node *tree.AlterIndex) (vitess.Statement, error) {
	if node == nil {
		return nil, nil
	}

	return NotYetSupportedError("ALTER INDEX is not yet supported")
}
⚠️ Legacy auth database load can break role mapping
  • What failed: Legacy auth metadata load is expected to preserve a valid role graph, but role identity state can drift after load and break core auth lookups.
  • Impact: Upgrades from legacy auth metadata can leave the server in a degraded authentication/authorization state where normal SQL access fails. This breaks a primary workflow with no practical user workaround.
  • Steps to reproduce:
    1. Prepare a data directory with a v1 auth.db fixture.
    2. Start doltgres with that directory and perform basic authenticated SQL operations.
    3. Observe repeated role lookup errors and degraded auth behavior.
  • Stub / mock context: The run intentionally used a legacy v1 auth metadata fixture to simulate upgrade behavior, and no route mocks or auth bypasses were used.
  • Code analysis: I inspected auth deserialization and role-ID assignment paths. deserializeV1 repopulates rolesByID but does not restore userIDCounter; new roles still use userIDCounter.Add(1) and SetRole overwrites existing ID slots, which can clobber built-in role mappings such as public.
  • Why this is likely a bug: The code permits ID collisions after legacy deserialization, and those collisions can delete or replace existing role-name mappings, which directly explains missing-role auth failures.

Relevant code:

server/auth/serialization.go (lines 108-118)

func (db *Database) deserializeV1(reader *utils.Reader) error {
	// Read the roles
	clear(db.rolesByName)
	clear(db.rolesByID)
	roleCount := reader.Uint32()
	for i := uint32(0); i < roleCount; i++ {
		r := Role{}
		r.deserialize(1, reader)
		db.rolesByName[r.Name] = r.id
		db.rolesByID[r.id] = r

server/auth/role.go (lines 47-50)

func CreateDefaultRole(name string) Role {
	r := createDefaultRoleWithoutID(name)
	r.id = RoleID(userIDCounter.Add(1))
	return r
}

server/auth/database.go (lines 116-120)

if existingRole, ok := globalDatabase.rolesByID[role.id]; ok {
	delete(globalDatabase.rolesByName, existingRole.Name)
}
globalDatabase.rolesByName[role.Name] = role.id
globalDatabase.rolesByID[role.ID()] = role
🚨 Malformed auth metadata can panic server startup
  • What failed: Malformed persistence input should be rejected with a controlled error path, but unchecked byte reads can trigger runtime panics and abort process startup.
  • Impact: A malformed auth metadata file can crash the server process during startup. This is a denial-of-service condition for affected deployments.
  • Steps to reproduce:
    1. Truncate auth.db to an undersized byte payload.
    2. Start doltgres with the tampered file in the configured data directory.
    3. Observe panic-style crash behavior instead of controlled rejection.
  • Stub / mock context: The test intentionally tampered auth metadata bytes to validate resilience against malformed persistence input, without route interception or bypass hooks.
  • Code analysis: Reader helpers perform direct byte indexing and slicing without bounds checks, so truncated payloads can raise index out of range; initialization then panics on deserialize errors instead of returning a recoverable startup error.
  • Why this is likely a bug: Production startup code has unchecked deserialization reads and panic-on-error handling, making malformed auth.db input capable of causing uncontrolled process termination.

Relevant code:

utils/reader.go (lines 40-43)

func (reader *Reader) Bool() bool {
	reader.offset += 1
	if reader.buf[reader.offset-1] == 1 {
		return true

utils/reader.go (lines 196-200)

func (reader *Reader) String() string {
	length := reader.VariableUint()
	reader.offset += length
	return string(reader.buf[reader.offset-length : reader.offset])
}

server/auth/database.go (lines 176-177)

} else if err = globalDatabase.deserialize(authData); err != nil {
	panic(err)
}

Commit: 2c6eb26

View Full Run


Tell us how we did: Give Ito Feedback

@itoqa

itoqa Bot commented Oct 8, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report — 2c6eb26 → d7166c6: 31 test cases ran, 1 new failure ❌, 2 still failing ❌, 26 passing ✅, 2 additional findings ⚠️.

Diff Summary

The run covers default access behavior for newly created tables, sequences, and routines, including owner and schema boundaries, privilege delegation, invalid commands, and persistence across restarts. It also exercises adversarial authorization-data conditions, revealing generally broad coverage but important failures in current-user handling, restart persistence, and damaged-state recovery.

Not safe to merge yet — this PR still has high-severity failures affecting authorization after restart and service availability when persisted authorization data is damaged, along with a medium-severity failure in the no-owner privilege form. Separate medium and minor findings appear unrelated to the PR and are flag-for-later caveats, but the attributable high-severity issues are merge blockers.

Tests run by Ito

View full run

Result State Severity Type Description
❌ ❌ New Failure High severity Serialization After restart, the new objects had empty access lists, and seq_reader could not read the readonly_user tables. The owner-specific default grants were not preserved for post-restart objects.
❌ ❌->❌ Still Failing High severity Serialization The server panicked while loading the damaged auth file, and no service remained available after restart.
❌ ❌->❌ Still Failing Medium severity Privileges The no-owner form of ALTER DEFAULT PRIVILEGES returned permission denied for with no role name. After the command failed, a new table could be created, but readonly_user could not read it because the default SELECT grant was never saved.
✅ Passing — Creation The new table was created with the configured default read access, and readonly_user successfully queried its row.
✅ Passing — Creation A default SELECT grant without a schema restriction let the read-only user query new tables in both public and alt.
✅ Passing — Creation A newly created sequence gave seq_reader access without a separate grant.
✅ Passing — Creation The function returned 42 and the procedure ran for func_reader after the default EXECUTE grant. Both new routines were usable without a separate object grant.
✅ Passing — Creation The configured owner’s new table was readable by readonly_user, while the table created by seq_reader stayed inaccessible.
✅ Passing — Creation The public table was readable by readonly_user, while the table in the other schema stayed inaccessible.
✅ Passing — Creation The configured default permission let func_reader run both newly created routines. The function returned 9 and the procedure call succeeded.
✅ Passing — Parser The single-owner command ran successfully, and the new table received the expected read access.
✅ Passing — Parser Both ROLE and USER forms kept auth_test_super as the owner, and readonly_user could read tables created after each grant.
✅ Passing — Parser The database accepted both partition commands and kept the parent and child tables available afterward.
✅ Passing — Parser The old form that names two owners is rejected with a clear syntax error at the comma.
✅ Passing — Parser Function, procedure, and routine grants were accepted, and func_reader successfully ran the new function and procedure.
✅ Passing — Privileges The default access rule was saved successfully, and the new table could be read by the intended user without a separate grant.
✅ Passing — Privileges The default-permission command was rejected, and a later table did not give readonly_user access.
✅ Passing — Privileges The new tables in schema_one and schema_two were readable by readonly_user. The table in schema_three stayed protected.
✅ Passing — Privileges The user can still read the new table, but cannot pass that access to another role.
✅ Passing — Privileges The mixed-grantee command failed because the named role did not exist. The table created before the separate valid grant stayed inaccessible, while the later table was readable after the valid grant.
✅ Passing — Privileges A regular user changed its own default table privileges and could read a new table through the inherited SELECT permission.
✅ Passing — Privileges A regular user was denied when targeting another owner, and no default privilege record was created.
✅ Passing — Privileges Both commands using missing roles returned a role-not-found error, and no default privilege records were created.
✅ Passing — Privileges The database rejected the invalid privilege and did not leave permissions on the new table.
✅ Passing — Serialization After the server restarted with the same data directory, readonly_user could read a newly created table using the saved default SELECT privilege.
✅ Passing — Serialization The server loaded the legacy version 1 authentication data, accepted a readonly user login, and completed a basic query as the administrator.
✅ Passing — Serialization The older auth file loaded successfully, and a table created afterward was not shared with readonly_user by default.
✅ Passing — Serialization Starting with an unknown authorization database format shows an upgrade-required error and leaves the database unavailable.
✅ Passing — Serialization After a restart, the saved authorization state loaded with the existing roles intact and a new table had no unexpected grants.
⚠️ Additional Finding Medium severity Parser The default SELECT grant with grant option was created, but submitting REVOKE GRANT OPTION FOR with RESTRICT returned an unsupported-operation error. The follow-up table read and delegation check therefore could not verify the required result.
⚠️ Additional Finding Minor severity Parser Both commands report success, but the system catalog returns zero rows for auth_test_super instead of showing the schema and type default mappings.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Revoke grant option is not supported
  • Severity: Medium Medium severity
  • Description: The default SELECT grant with grant option was created, but submitting REVOKE GRANT OPTION FOR with RESTRICT returned an unsupported-operation error. The follow-up table read and delegation check therefore could not verify the required result.
  • Impact: Database administrators cannot use this revoke command to remove delegation rights while keeping a user's existing access. The permission change is not applied, so the user may retain the ability to grant that access to others until an alternate fix is used.
  • Steps to Reproduce:
    1. Connect as auth_test_super.
    2. Create a default SELECT privilege with grant option for readonly_user.
    3. Run the matching REVOKE GRANT OPTION FOR command with RESTRICT.
    4. Create a new table, check that readonly_user can still read it, and try to delegate SELECT from readonly_user.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The parser represents this operation explicitly in postgres/parser/sem/tree/alter_default_privileges.go:27-35 and formats the REVOKE, GRANT OPTION FOR, and RESTRICT components at lines 62-73. The grammar in postgres/parser/parser/sql.y:1914-1916 also constructs an AlterDefaultPrivileges node with GrantOption and DropBehavior, so these tokens are accepted into the AST. The default-privilege executor in server/node/alter_default_privileges.go:134-138 passes a revoke and grant-option-only flag to auth.RemoveDefaultPrivilege. That auth function is implemented in server/auth/default_privileges.go:79-123 and documents that grantOptionOnly should clear only the WITH GRANT OPTION flag while retaining the privilege. However, the observed execution returns the exact error from server/ast/revoke.go:144-146, where the converter rejects an otherwise valid REVOKE form with this form of REVOKE is not yet supported. This leaves the requested revoke operation unapplied. The smallest practical fix is to route the parsed target and grant-option-only flag through the supported revoke/default-privilege execution path instead of returning from the unsupported-form branch, then add a regression test for retaining SELECT while denying delegation. The available PR context does not provide a verifiable diff for the rejecting converter path, so this finding is not attributed to the PR.
Evidence Package
⚪ Schema and type defaults disappear
  • Severity: Minor Minor severity
  • Description: Both commands report success, but the system catalog returns zero rows for auth_test_super instead of showing the schema and type default mappings.
  • Impact: Administrators cannot confirm schema and type default privileges through the system catalog. The privileges are still stored and applied, so this mainly blocks reliable inspection.
  • Steps to Reproduce:
    1. Connect to the local server as the seeded auth_test_super role.
    2. Run ALTER DEFAULT PRIVILEGES FOR ROLE auth_test_super GRANT USAGE ON SCHEMAS TO readonly_user.
    3. Run ALTER DEFAULT PRIVILEGES FOR ROLE auth_test_super GRANT USAGE ON TYPES TO readonly_user.
    4. Query pg_catalog.pg_default_acl for auth_test_super and check the returned rows.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The conversion path in server/ast/alter_default_privileges.go accepts privilege.Schema and privilege.Type and maps them to auth.PrivilegeObject_SCHEMA and auth.PrivilegeObject_TYPE (lines 63-76). server/node/alter_default_privileges.go then builds a DefaultPrivilegeKey with the owner role, schema scope, and object type (lines 116-127), and calls auth.AddDefaultPrivilege for each grantee and privilege (lines 128-137), so the commands have a real in-memory persistence path rather than being parser-only no-ops. The readback failure is explained by server/tables/pgcatalog/pg_default_acl.go: PgDefaultAclHandler.RowIter always calls emptyRowIter (lines 44-50), and its iterator's Next method immediately returns io.EOF (lines 69-77), never reading auth.GetAllDefaultPrivileges or converting entries into catalog rows. The smallest fix is to make this handler enumerate the stored default-privilege entries and emit the matching owner, namespace, object-type, and ACL values; it does not require changing the parser or unrelated object-creation hooks.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread server/auth/serialization.go
@itoqa

itoqa Bot commented Oct 9, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report — d7166c6 → 53ffcc0: 21 test cases ran, 1 new failure ❌, 1 still failing ❌, 2 fixed ✅, 15 passing ✅, 2 additional findings ⚠️.

Diff Summary

The run covers database cursor behavior across normal movement, lifecycle, cleanup, and parameterized access, along with enum defaults, privilege inheritance and authorization boundaries, parsing, and persisted authorization state. It exercises both successful business flows and adversarial or recovery cases, with broad coverage of protocol, catalog, and permission behavior.

Merge with caution — two medium-severity issues attributable to this PR remain: parameterized cursor access is broken and configured schema/type defaults are not visible through the catalog, creating functional and administration risks. Separate medium findings in built-in function discovery and malformed authorization-data handling are not attributable to this PR and are caveats rather than merge drivers.

Tests run by Ito

View full run

Result State Severity Type Description
❌ ❌ New Failure Medium severity Parser The valid bound cursor declaration returned a syntax error at EOF, so no cursor was created and the following fetch was rejected because the transaction was already aborted. The malformed binding also returned a controlled syntax error, and the later rollback removed the transaction state.
❌ ❌->❌ Still Failing Medium severity Parser The commands are accepted, but the saved schema and type mappings do not appear in the default-privilege catalog.
✅ ❌->✅ Fixed — Parser Verified acceptable by independent adversarial review: the scenario cannot be reached through any real application path. Review notes: The obligation is real, but the finding traces the statement through the wrong AST type. The grammar constructs tree.AlterDefaultPrivileges, its dedicated converter forwards GrantOption, and its executor reaches RemoveDefaultPrivilege, whose grant-option-only branch retains the privilege; therefore the cited nodeRevoke unsupported-form fallthrough is unreachable for the asserted ALTER DEFAULT PRIV…
✅ ❌->✅ Fixed — Privileges A non-superuser ran ALTER DEFAULT PRIVILEGES without naming an owner, and a new table granted SELECT to the reader role.
✅ Passing — Cursor The transaction cursor was created successfully and returned rows 1 through 3 in the expected order.
✅ Passing — Cursor An idle cursor declaration was rejected, and a duplicate name did not replace the original cursor. After savepoint recovery, the original cursor still returned its row.
✅ Passing — Cursor The held cursor stayed available after commit, while the local and rolled-back cursors were removed as expected.
✅ Passing — Cursor The cursor catalog showed only cursors that could still be used. Cursors removed by commit or rollback disappeared, and the released name could be used again without confusion.
✅ Passing — Cursor Cursor movement returned the expected rows for forward, relative, absolute, zero, negative, and ALL directions.
✅ Passing — Cursor Moving the cursor returned only the number of rows moved, and the next fetch returned the row at the new position.
✅ Passing — Cursor The cursor reached the end, moved backward without skipping rows, and returned to the first row after moving backward through all remaining rows.
✅ Passing — Cursor FETCH returned the requested rows with their column names, while MOVE returned only the number of rows moved. This stayed correct when the cursor reached the end of its data.
✅ Passing — Enum The enum column was added successfully, and new rows received the default value x.
✅ Passing — Enum The invalid enum value was rejected without adding a column, and the next valid change and insert succeeded.
✅ Passing — Parser The cursor was declared, rows were fetched, movement returned its count, and the cursor closed successfully.
✅ Passing — Parser The simple and extended database protocols both created and read the cursor correctly. The parameterized query returned rows 4,d and 5,e, with the expected completion messages and no wrapper statement exposed.
✅ Passing — Privilege The configured reader could query tables created by both owners, so the default SELECT grant was applied to the full owner list.
✅ Passing — Privilege A non-owner could run the first function using the default PUBLIC access. After access was revoked for future functions, the same non-owner was denied access to the second function.
✅ Passing — Privilege A non-owner could not change default permissions for another owner. The mixed-owner command was rejected, no default grant was created, and the reader could not access either owner's new table.
⏸️ Skipped — Creation The new table was created with the configured default read access, and readonly_user successfully queried its row.
⏸️ Skipped — Creation A default SELECT grant without a schema restriction let the read-only user query new tables in both public and alt.
⏸️ Skipped — Creation A newly created sequence gave seq_reader access without a separate grant.
⏸️ Skipped — Creation The function returned 42 and the procedure ran for func_reader after the default EXECUTE grant. Both new routines were usable without a separate object grant.
⏸️ Skipped — Creation The configured owner’s new table was readable by readonly_user, while the table created by seq_reader stayed inaccessible.
⏸️ Skipped — Creation The public table was readable by readonly_user, while the table in the other schema stayed inaccessible.
⏸️ Skipped — Creation The configured default permission let func_reader run both newly created routines. The function returned 9 and the procedure call succeeded.
⏸️ Skipped — Parser The single-owner command ran successfully, and the new table received the expected read access.
⏸️ Skipped — Parser Both ROLE and USER forms kept auth_test_super as the owner, and readonly_user could read tables created after each grant.
⏸️ Skipped — Parser The database accepted both partition commands and kept the parent and child tables available afterward.
⏸️ Skipped — Parser The old form that names two owners is rejected with a clear syntax error at the comma.
⏸️ Skipped — Parser Function, procedure, and routine grants were accepted, and func_reader successfully ran the new function and procedure.
⏸️ Skipped — Privileges The default access rule was saved successfully, and the new table could be read by the intended user without a separate grant.
⏸️ Skipped — Privileges The default-permission command was rejected, and a later table did not give readonly_user access.
⏸️ Skipped — Privileges The new tables in schema_one and schema_two were readable by readonly_user. The table in schema_three stayed protected.
⏸️ Skipped — Privileges The user can still read the new table, but cannot pass that access to another role.
⏸️ Skipped — Privileges The mixed-grantee command failed because the named role did not exist. The table created before the separate valid grant stayed inaccessible, while the later table was readable after the valid grant.
⏸️ Skipped — Privileges A regular user changed its own default table privileges and could read a new table through the inherited SELECT permission.
⏸️ Skipped — Privileges A regular user was denied when targeting another owner, and no default privilege record was created.
⏸️ Skipped — Privileges Both commands using missing roles returned a role-not-found error, and no default privilege records were created.
⏸️ Skipped — Privileges The database rejected the invalid privilege and did not leave permissions on the new table.
⏸️ Skipped — Serialization After the server restarted with the same data directory, readonly_user could read a newly created table using the saved default SELECT privilege.
⏸️ Skipped — Serialization The server loaded the legacy version 1 authentication data, accepted a readonly user login, and completed a basic query as the administrator.
⏸️ Skipped — Serialization The older auth file loaded successfully, and a table created afterward was not shared with readonly_user by default.
⏸️ Skipped — Serialization Starting with an unknown authorization database format shows an upgrade-required error and leaves the database unavailable.
⏸️ Skipped — Serialization After a restart, the saved authorization state loaded with the existing roles intact and a new table had no unexpected grants.
⚠️ Additional Finding Medium severity Parse The direct call returns {SomeSchema,some_table}, but the catalog query returns no parse_ident overload rows. The function is callable without being discoverable through the catalog.
⚠️ Additional Finding Medium severity Serialization Starting with a truncated authorization database did not produce a controlled malformed-file rejection in the recorded run; the source path also shows that a deserialization error is converted into a process panic.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Function lookup misses built-in overloads
  • Severity: Medium Medium severity
  • Description: The direct call returns {SomeSchema,some_table}, but the catalog query returns no parse_ident overload rows. The function is callable without being discoverable through the catalog.
  • Impact: Tools that inspect the database catalog cannot discover the built-in identifier parser, so catalog-based calls may fail even though direct calls work. Users can still call the function directly, but clients that rely on function metadata lose expected compatibility.
  • Steps to Reproduce:
    1. Connect to the local database with the normal PostgreSQL client.
    2. Query pg_catalog.pg_proc for functions named parse_ident in the pg_catalog schema.
    3. Call pg_catalog.parse_ident('"SomeSchema".some_table', false) and compare the result with the catalog lookup.
    4. Try resolving the text,bool overload through the function metadata or a bound-parameter call.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The new implementation in server/functions/parse_ident.go defines parse_ident_text with a text parameter and parse_ident_text_bool with text,bool parameters, and server/functions/init.go calls initParseIdent during function initialization. The PR also assigns OID 1268 to the pg_catalog parse_ident(text,bool) cache entry in core/id/cache_function_defaults.go. However, server/tables/pgcatalog/pg_proc.go:94-96 builds pg_proc from functions.IterateCurrentDatabase and has the explicit TODO 'add built-in functions'; its callback only appends database-defined functions. As a result, the newly registered built-ins are executable through function dispatch but are absent from pg_proc, matching the observed zero-row catalog lookup. The smallest practical fix is to include registered built-in functions in the pg_proc cache, or add the parse_ident overload rows there, while preserving their OIDs, argument types, and return type.
Evidence Package
🟡 Malformed auth data can crash startup
  • Severity: Medium Medium severity
  • Description: Starting with a truncated authorization database did not produce a controlled malformed-file rejection in the recorded run; the source path also shows that a deserialization error is converted into a process panic.
  • Impact: If the authorization database is damaged, the server may fail to start or panic instead of reporting a clear startup error. Users may be unable to sign in until an operator restores a valid database.
  • Steps to Reproduce:
    1. Stop the local server and back up the authorization database file.
    2. Replace the configured auth.db with three zero bytes or another truncated serialized payload.
    3. Start the server with that data directory and inspect the startup result and process logs.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/auth/database.go:271-295 initializes globalDatabase, reads authFileName, and calls globalDatabase.deserialize(authData). Any returned error reaches panic(err) at line 294. server/auth/serialization.go:67-70 correctly detects a payload shorter than four bytes and returns errors.New("invalid auth database format"), but the caller does not propagate that error as a normal startup error. For longer truncated payloads, the same risk is worse: deserializeV2 calls the collection deserializers without error returns, while server/auth/default_privileges.go:378-419 reads counts and fields from utils.Reader. utils.Reader's Uint32 and Uint64 methods index the backing byte slice without bounds checks (utils/reader.go:81-90), so an incomplete payload can panic before deserialize returns. The test evidence reported a three-byte fixture, a startup log claiming the integrity check passed, and an inconclusive second restart because the prior process remained active; that makes the runtime result partially inconclusive, but the production loading path independently establishes the uncontrolled-panic defect when the configured auth file is actually read. The smallest fix is to make all auth deserializers return and propagate truncation errors, then have dbInit return an annotated startup error instead of panicking; do not silently initialize or continue with partial authorization state.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

@itoqa

itoqa Bot commented Oct 9, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report — 53ffcc0 → dbdbdf4: 11 test cases ran, 1 new failure ❌, 2 fixed ✅, 4 passing ✅, 4 additional findings ⚠️.

Diff Summary

Coverage spans database command parsing and cursor behavior, permission setup and delegation across object types, configuration persistence, concurrency, cleanup, and adversarial recovery cases involving malformed data and routine updates. Happy-path and edge-case behavior was broadly exercised, but routine replacement exposed a serious failure in an unsafe update path.

Not safe to merge yet — this PR introduces a high-severity failure in which a malformed routine replacement can discard the working operation and crash the server, making it a merge blocker. Separate medium findings about privilege and catalog behavior are marked unrelated to this PR and should be treated as follow-up caveats rather than merge drivers.

Tests run by Ito

View full run

Result State Severity Type Description
❌ ❌ New Failure High severity Privilege The replacement statement reported success instead of failing cleanly. The next call did not return the original value; it triggered a DoltgresHandler panic, so the original routine was not preserved atomically.
✅ ❌->✅ Fixed — Parser A valid parameter filtered the cursor to rows 4,d and 5,e. An invalid parameter binding returned a controlled SQL error without creating the cursor.
✅ ❌->✅ Fixed — Serialization Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The truncated-file mechanism is credible: startup reads auth.db, deserialization reaches unchecked Reader accesses, and malformed bytes can panic. The finding nevertheless rests on an obligation that the repository directly contradicts: dbInit documents fatal panic as the intended response to initialization errors and explicitly panics on a returned deserialization error, so requiring a non-crashi…
✅ Passing — Privilege Database, schema, table, sequence, function, and procedure grants kept their delegation rights after a later ordinary grant. Granting the option second also allowed delegation.
✅ Passing — Privilege Running CREATE TABLE IF NOT EXISTS with a different definition left the existing table unchanged. Its columns, access rules, and identity sequence remained intact, and the newly configured grantee did not receive access.
✅ Passing — Privilege Two sessions granted the same table privilege at the same time, and the grantee could still pass that privilege to another role.
✅ Passing — Privilege The user could still read the table after the grant option was revoked, but could no longer pass that access to another role.
⏸️ Skipped — Cursor The transaction cursor was created successfully and returned rows 1 through 3 in the expected order.
⏸️ Skipped — Cursor An idle cursor declaration was rejected, and a duplicate name did not replace the original cursor. After savepoint recovery, the original cursor still returned its row.
⏸️ Skipped — Cursor The held cursor stayed available after commit, while the local and rolled-back cursors were removed as expected.
⏸️ Skipped — Cursor The cursor catalog showed only cursors that could still be used. Cursors removed by commit or rollback disappeared, and the released name could be used again without confusion.
⏸️ Skipped — Cursor Cursor movement returned the expected rows for forward, relative, absolute, zero, negative, and ALL directions.
⏸️ Skipped — Cursor Moving the cursor returned only the number of rows moved, and the next fetch returned the row at the new position.
⏸️ Skipped — Cursor The cursor reached the end, moved backward without skipping rows, and returned to the first row after moving backward through all remaining rows.
⏸️ Skipped — Cursor FETCH returned the requested rows with their column names, while MOVE returned only the number of rows moved. This stayed correct when the cursor reached the end of its data.
⏸️ Skipped — Enum The enum column was added successfully, and new rows received the default value x.
⏸️ Skipped — Enum The invalid enum value was rejected without adding a column, and the next valid change and insert succeeded.
⏸️ Skipped — Parser The cursor was declared, rows were fetched, movement returned its count, and the cursor closed successfully.
⏸️ Skipped — Parser The simple and extended database protocols both created and read the cursor correctly. The parameterized query returned rows 4,d and 5,e, with the expected completion messages and no wrapper statement exposed.
⏸️ Skipped — Parser Verified acceptable by independent adversarial review: the scenario cannot be reached through any real application path. Review notes: The obligation is real, but the finding traces the statement through the wrong AST type. The grammar constructs tree.AlterDefaultPrivileges, its dedicated converter forwards GrantOption, and its executor reaches RemoveDefaultPrivilege, whose grant-option-only branch retains the privilege; therefore the cited nodeRevoke unsupported-form fallthrough is unreachable for the asserted ALTER DEFAULT PRIV…
⏸️ Skipped — Privilege The configured reader could query tables created by both owners, so the default SELECT grant was applied to the full owner list.
⏸️ Skipped — Privilege A non-owner could run the first function using the default PUBLIC access. After access was revoked for future functions, the same non-owner was denied access to the second function.
⏸️ Skipped — Privilege A non-owner could not change default permissions for another owner. The mixed-owner command was rejected, no default grant was created, and the reader could not access either owner's new table.
⏸️ Skipped — Privileges A non-superuser ran ALTER DEFAULT PRIVILEGES without naming an owner, and a new table granted SELECT to the reader role.
⚠️ Additional Finding Medium severity Parser The two ALTER DEFAULT PRIVILEGES statements returned success, but pg_default_acl returned zero rows for the schema and type mappings, and the full catalog readback was also empty.
⚠️ Additional Finding Medium severity Privilege After the owner configured default USAGE and SELECT access for the reader, the reader could query the empty table but received a table-not-found error when reading the new standalone sequence.
⚠️ Additional Finding Medium severity Privilege The two calculate overloads were created, but the configured reader default permission was not visible for the new overload. The permission code does not retain the argument types needed to keep the two signatures separate.
⚠️ Additional Finding Medium severity Privilege After global and schema-specific function permissions were configured, the created functions had no recorded permissions and the default-permission catalog had no rows. The later revoke-and-create check showed the same empty permission state.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Default privilege mappings are not saved
  • Severity: Medium Medium severity
  • Description: The two ALTER DEFAULT PRIVILEGES statements returned success, but pg_default_acl returned zero rows for the schema and type mappings, and the full catalog readback was also empty.
  • Impact: Users can create the default privilege mappings, but the database catalog shows no rows for them. This makes the configuration hard to inspect and can mislead tools that rely on the catalog, while the underlying mappings may still affect new objects.
  • Steps to Reproduce:
    1. Create a role and an isolated schema.
    2. Grant USAGE on SCHEMAS to the role with ALTER DEFAULT PRIVILEGES in that schema.
    3. Grant USAGE on TYPES to the same role with ALTER DEFAULT PRIVILEGES in that schema.
    4. Query pg_default_acl for the isolated schema and inspect the full catalog readback.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The SQL conversion path accepts both object types in server/ast/alter_default_privileges.go:73-76, mapping Schema to auth.PrivilegeObject_SCHEMA and Type to auth.PrivilegeObject_TYPE. server/node/alter_default_privileges.go:125-145 then builds a DefaultPrivilegeKey for each owner, schema, and object type, calls auth.AddDefaultPrivilege, and persists the auth database through auth.PersistChanges at lines 65-76. The in-memory persistence path is real: server/auth/default_privileges.go:53-78 stores the key and grantee privileges in globalDatabase.defaultPrivileges.Data, and lines 353-375 serialize those entries. However, server/tables/pgcatalog/pg_default_acl.go:44-49 unconditionally returns emptyRowIter and explicitly says the catalog is empty because ALTER DEFAULT PRIVILEGES was unsupported. That handler is not part of the PR diff. Consequently, the statements can succeed and affect auth-backed object creation while catalog queries silently show no rows. The smallest practical fix is to make PgDefaultAclHandler.RowIter enumerate auth.GetAllDefaultPrivileges(), converting each key and ACL into the pg_default_acl columns, rather than returning an unconditional empty iterator.
Evidence Package
🟡 New sequence access is not granted
  • Severity: Medium Medium severity
  • Description: After the owner configured default USAGE and SELECT access for the reader, the reader could query the empty table but received a table-not-found error when reading the new standalone sequence.
  • Impact: Database users who should receive default sequence access cannot read newly created standalone sequences. An administrator can restore access with a manual grant, but each affected sequence needs extra work.
  • Steps to Reproduce:
    1. Create an owner role, a reader role, and a schema, then give the owner permission to create objects in that schema.
    2. As the superuser, configure ALTER DEFAULT PRIVILEGES for the owner to grant USAGE and SELECT on sequences to the reader.
    3. As the owner, create a standalone sequence and an identity-backed table in the schema.
    4. Switch to the reader and read the table and the standalone sequence.
    5. The table query succeeds, but reading the standalone sequence fails with a table-not-found error.
  • Stub / mock content: The test used isolated local SQL roles, schemas, and database objects. The browser check was not applicable because the target exposes a PostgreSQL wire protocol rather than an HTTP page; no application responses or privilege calculations were mocked.
  • Code Analysis: The production path intends to apply sequence defaults, but the recorded behavior does not satisfy that contract. In server/node/create_sequence.go:245-247, every newly created sequence calls applyDefaultPrivilegesForNewObject with auth.ApplyDefaultPrivilegesForNewSequence. That helper in server/node/alter_default_privileges.go:175-193 resolves the current role, runs the callback under the write lock, persists changes when privileges were added, and waits for replication. The callback in server/auth/default_privileges.go:268-290 filters stored defaults by the creating owner's role, object type, and schema, then writes a SequencePrivilegeKey for the configured grantee and sequence name through AddSequencePrivilege. Despite that path, the final reader check at the recorded run failed for privilege18_f.standalone_seq. This is a plausible production defect in the sequence privilege application or sequence privilege enforcement path, not evidence that the SQL command was rejected. The empty relacl/proacl values are a separate catalog limitation: server/tables/pgcatalog/pg_proc.go:358 always returns nil for proacl and server/tables/pgcatalog/pg_default_acl.go:46-49 returns an empty iterator, so those metadata fields cannot independently prove that the internal privilege store is empty. The smallest practical fix is to trace the created sequence's owner/schema/name through ApplyDefaultPrivilegesForNewSequence and the sequence lookup/check path, then ensure the stored grant is associated with the same qualified sequence relation used by reader access; add a regression assertion for a schema-qualified standalone sequence alongside the existing new-sequence test.
Evidence Package
🟡 New routine overloads cannot keep separate default grants
  • Severity: Medium Medium severity
  • Description: The two calculate overloads were created, but the configured reader default permission was not visible for the new overload. The permission code does not retain the argument types needed to keep the two signatures separate.
  • Impact: Database administrators cannot reliably set different default permissions for overloaded routines. A new routine may not receive the intended access rule, so administrators may need to grant permissions manually to keep access limited as intended.
  • Steps to Reproduce:
    1. Create an owner role, a reader role, and a schema owned by the owner.
    2. Grant the reader EXECUTE by default for functions created in that schema.
    3. Create calculate(integer), then create the distinct calculate(text) overload.
    4. Inspect both routine identities and their permissions, then call each overload as the reader.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/node/create_function.go:143-180 correctly builds funcID with the input parameter types and calls applyDefaultPrivilegesForNewObject for a routine that is not an exact replacement. However, server/auth/default_privileges.go:293-332 implements ApplyDefaultPrivilegesForNewRoutine with only ownerRoleID, schemaName, and routineName; every RoutinePrivilegeKey it creates at lines 303-307 and 323-327 leaves ArgTypes empty. server/auth/routine_privileges.go:26-33 defines ArgTypes as the field that identifies a routine signature, so the default-application path cannot represent the new text overload independently from the existing integer overload. The focused repository test at testing/go/alter_default_privileges_test.go:560-578 is skipped specifically because RoutinePrivilegeKey.ArgTypes is not supported. The recorded SQL created both overloads but showed NULL proacl and an empty pg_default_acl readback, so the run did not prove the exact per-signature ACL outcome; source inspection still supports the defect hypothesis, which is why this row is marked previously_omitted. The smallest fix is to pass the new routine's input type identity into ApplyDefaultPrivilegesForNewRoutine and populate ArgTypes in each generated RoutinePrivilegeKey, then enable the focused overload assertions.
Evidence Package
🟡 Function default permissions are not visible after setup
  • Severity: Medium Medium severity
  • Description: After global and schema-specific function permissions were configured, the created functions had no recorded permissions and the default-permission catalog had no rows. The later revoke-and-create check showed the same empty permission state.
  • Impact: Users and database tools cannot reliably see the default or routine permissions they configured. This can hide privilege changes and break workflows that depend on PostgreSQL-compatible privilege checks, but the test did not show an authorization bypass or data exposure.
  • Steps to Reproduce:
    1. Configure a global PUBLIC EXECUTE default for functions and a different default for one schema.
    2. Create a function in that schema and inspect its permissions and the default-permission catalog.
    3. Revoke PUBLIC EXECUTE, create a second function, and inspect both permission states again.
    4. Observe that both functions have an empty permission field and the default-permission catalog remains empty; an effective permission query is unavailable because has_function_privilege is not implemented.
  • Stub / mock content: The test used a local nested Doltgres server and repaired schema access with explicit USAGE and CREATE grants after the fixture's owner metadata did not provide access. No application mocks or route stubs were used.
  • Code Analysis: The production path accepts ALTER DEFAULT PRIVILEGES in server/node/alter_default_privileges.go:103-147 and stores the requested entries in auth.DefaultPrivileges through auth.AddDefaultPrivilege and auth.RemoveDefaultPrivilege. New functions call auth.ApplyDefaultPrivilegesForNewRoutine from server/node/create_function.go:173-180, which adds matching routine privileges for both the global and schema-specific keys in server/auth/default_privileges.go:293-332. However, the SQL-visible catalog does not expose that state: server/tables/pgcatalog/pg_default_acl.go:44-50 unconditionally returns an empty iterator and explicitly leaves the table unimplemented, while server/tables/pgcatalog/pg_proc.go:328-358 emits nil for proacl for every routine. The recorded SQL therefore accepted the configuration but returned zero pg_default_acl rows and empty proacl for both f_global_schema and f_after_revoke. The missing has_function_privilege function also prevented an independent effective-authorization assertion. The smallest practical fix is to expose the stored default-privilege and routine-privilege state through pg_default_acl, pg_proc.proacl, and the supported has_function_privilege overloads; the existing internal application path should then be tested against those projections.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

@@ -140,7 +141,9 @@ func (c *CreateFunction) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, erro
return nil, err
}
funcID := id.NewFunction(schemaName, c.FunctionName, inputParamTypes...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

🆕 New Failure: identified in this diff run

High severity Malformed replacement breaks the original routine

What failed: The replacement statement reported success instead of failing cleanly. The next call did not return the original value; it triggered a DoltgresHandler panic, so the original routine was not preserved atomically.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: High High severity
  • Impact: A malformed routine update can remove the working routine and make later calls crash the database server instead of returning an error. Users and applications that rely on that routine may lose access to the affected operation until it is repaired.
  • Steps to Reproduce:
    1. Create a function that returns 42 and grant a reader role permission to execute it.
    2. Call the function once and confirm that it returns 42.
    3. Submit a malformed CREATE OR REPLACE FUNCTION statement that reaches the replacement path.
    4. Call the routine again and inspect the server response and catalog entry.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The PR changed server/node/create_function.go at lines 144-150 to compute replaced and immediately call funcCollection.DropFunction before the replacement has passed the extension lookup at lines 151-155 or the new funcCollection.AddFunction call at lines 156-171. If either later stage fails, there is no rollback or re-add of the prior function, which is a direct partial-update path. The corresponding procedure implementation has the same ordering at server/node/create_procedure.go lines 125-137, but this test exercised the function path. The captured server stack identifies resultForDefaultIter in server/doltgres_handler.go line 704 and int4out in server/functions/int4.go line 68, where val.(int32) panics after the malformed replacement produces an invalid value shape. The practical fix is to perform replacement validation and construction before removing the old entry, or retain the old function and restore its definition and privileges on every post-drop error; the existing ACL must also remain attached to the replacement.
  • Why this is likely a bug: The failure is not limited to a missing test expectation: a malformed update was accepted and a later ordinary call caused an unhandled server panic instead of returning a database error. Existing repository coverage explicitly treats CREATE OR REPLACE as preserving a callable routine and its existing ACL. The PR's drop-before-add ordering creates the partial replacement window, and the recorded stack confirms that the resulting invalid routine data reaches a production type assertion. A targeted transactional or rollback-safe replacement change is sufficient; no broad privilege redesign is needed.
Relevant code

server/node/create_function.go:143-171

funcID := id.NewFunction(schemaName, c.FunctionName, inputParamTypes...)
replaced := c.Replace && funcCollection.HasFunction(ctx, funcID)
if replaced {
	if err = funcCollection.DropFunction(ctx, funcID); err != nil {
		return nil, err
	}
}
if len(c.ExtensionName) > 0 {
	if _, err = extensions.GetFunction(c.ExtensionName, c.ExtensionSymbol); err != nil {
		return nil, err
	}
}
err = funcCollection.AddFunction(ctx, functions.Function{

server/doltgres_handler.go:697-705

pan2err := func(err *error) {
	if HandlePanics {
		if recoveredPanic := recover(); recoveredPanic != nil {
			*err = goerrors.Join(*err, errors.Errorf("DoltgresHandler caught panic: %v: %s", recoveredPanic, debug.Stack()))
		}
	}
}

server/functions/int4.go:61-69

var int4out = framework.Function1{
	Name:       "int4out",
	Return:     pgtypes.Cstring,
	Parameters: [1]*pgtypes.DoltgresType{pgtypes.Int32},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
		return strconv.FormatInt(int64(val.(int32)), 10), nil
	},

testing/go/alter_default_privileges_test.go:536-551

Name: "CREATE OR REPLACE FUNCTION preserves existing ACL instead of applying new defaults"
...
{Query: `CREATE OR REPLACE FUNCTION replace_existing() RETURNS INT AS $$ BEGIN RETURN 2; END; $$ LANGUAGE plpgsql;`},
{Query: `SELECT replace_existing();`, Username: "replace_reader", Password: "a", Expected: []sql.Row{{2}}},
{Query: `SELECT replace_existing();`, Username: "replace_new_reader", Password: "a", ExpectedErr: "denied"},
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**High severity — Malformed replacement breaks the original routine**

**What failed:** The replacement statement reported success instead of failing cleanly. The next call did not return the original value; it triggered a DoltgresHandler panic, so the original routine was not preserved atomically.

- **Impact:** A malformed routine update can remove the working routine and make later calls crash the database server instead of returning an error. Users and applications that rely on that routine may lose access to the affected operation until it is repaired.
- **Steps to reproduce:**
  1. Create a function that returns 42 and grant a reader role permission to execute it.
  2. Call the function once and confirm that it returns 42.
  3. Submit a malformed CREATE OR REPLACE FUNCTION statement that reaches the replacement path.
  4. Call the routine again and inspect the server response and catalog entry.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The PR changed server/node/create_function.go at lines 144-150 to compute `replaced` and immediately call `funcCollection.DropFunction` before the replacement has passed the extension lookup at lines 151-155 or the new `funcCollection.AddFunction` call at lines 156-171. If either later stage fails, there is no rollback or re-add of the prior function, which is a direct partial-update path. The corresponding procedure implementation has the same ordering at server/node/create_procedure.go lines 125-137, but this test exercised the function path. The captured server stack identifies resultForDefaultIter in server/doltgres_handler.go line 704 and int4out in server/functions/int4.go line 68, where `val.(int32)` panics after the malformed replacement produces an invalid value shape. The practical fix is to perform replacement validation and construction before removing the old entry, or retain the old function and restore its definition and privileges on every post-drop error; the existing ACL must also remain attached to the replacement.
- **Why this is likely a bug:** The failure is not limited to a missing test expectation: a malformed update was accepted and a later ordinary call caused an unhandled server panic instead of returning a database error. Existing repository coverage explicitly treats CREATE OR REPLACE as preserving a callable routine and its existing ACL. The PR's drop-before-add ordering creates the partial replacement window, and the recorded stack confirms that the resulting invalid routine data reaches a production type assertion. A targeted transactional or rollback-safe replacement change is sufficient; no broad privilege redesign is needed.

**Relevant code:**

`server/node/create_function.go:143-171`

~~~go
funcID := id.NewFunction(schemaName, c.FunctionName, inputParamTypes...)
replaced := c.Replace && funcCollection.HasFunction(ctx, funcID)
if replaced {
	if err = funcCollection.DropFunction(ctx, funcID); err != nil {
		return nil, err
	}
}
if len(c.ExtensionName) > 0 {
	if _, err = extensions.GetFunction(c.ExtensionName, c.ExtensionSymbol); err != nil {
		return nil, err
	}
}
err = funcCollection.AddFunction(ctx, functions.Function{
~~~

`server/doltgres_handler.go:697-705`

~~~go
pan2err := func(err *error) {
	if HandlePanics {
		if recoveredPanic := recover(); recoveredPanic != nil {
			*err = goerrors.Join(*err, errors.Errorf("DoltgresHandler caught panic: %v: %s", recoveredPanic, debug.Stack()))
		}
	}
}
~~~

`server/functions/int4.go:61-69`

~~~go
var int4out = framework.Function1{
	Name:       "int4out",
	Return:     pgtypes.Cstring,
	Parameters: [1]*pgtypes.DoltgresType{pgtypes.Int32},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
		return strconv.FormatInt(int64(val.(int32)), 10), nil
	},
~~~

`testing/go/alter_default_privileges_test.go:536-551`

~~~go
Name: "CREATE OR REPLACE FUNCTION preserves existing ACL instead of applying new defaults"
...
{Query: `CREATE OR REPLACE FUNCTION replace_existing() RETURNS INT AS $$ BEGIN RETURN 2; END; $$ LANGUAGE plpgsql;`},
{Query: `SELECT replace_existing();`, Username: "replace_reader", Password: "a", Expected: []sql.Row{{2}}},
{Query: `SELECT replace_existing();`, Username: "replace_new_reader", Password: "a", ExpectedErr: "denied"},
~~~

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.

2 participants