Add visibility-based defaults for model access levels

What does this MR do and why?

New projects now get visibility-based defaults for the model registry and model experiments access levels, mirroring how pages_access_level works:

  • Public projects → Enabled (Everyone With Access)
  • Internal / Private projects → Private (Only Project Members)

Previously both features always defaulted to Enabled regardless of project visibility.

Implementation:

  • Drop the DB-level DEFAULT 20 on project_features.model_registry_access_level and model_experiments_access_level (metadata-only migration; existing rows untouched).
  • Compute the default in an after_initialize callback on ProjectFeature, so a new record starts NULL and the callback fills in the value from project visibility (explicitly-provided values are preserved).
  • Update the development seed fixture to set these columns explicitly, since it relied on the dropped default.

Steps to reproduce

  1. Run the migrations.
  2. Create a new public project. Open Settings > General, expand Visibility, project features, permissions. The Model registry and Model experiments access dropdowns show Everyone With Access.
  3. Create a new private project (and repeat for internal). In the same settings, Model registry and Model experiments show Only Project Members.

Before this change, all three visibilities showed Everyone With Access.

Does this affect existing behavior?

No. ProjectPolicy has a carve-out that, on non-public projects, only grants read_model_registry / read_model_experiments to members (guest and above), regardless of the feature's access level:

rule { ~public_project & guest & model_registry_enabled }.enable :read_model_registry

So non-members were already prevented from reading these features on internal/private projects even while the level defaulted to Enabled. Lowering the default to Private doesn't change who can read — it just aligns the stored default (and the settings dropdown above) with what the policy already enforces.

References

#603262 (closed)

Migration

Post-deployment migration. The default is changed in a post-deploy migration (and the model declares columns_changing_default via SafelyChangeColumnDefault) per Changing column defaults. change_column_default is a metadata-only operation in PostgreSQL — no table rewrite, existing rows untouched. Runs identically on the main, ci, and sec databases; main output shown below.

Up

$ bundle exec rails db:migrate:up:main VERSION=20260617130000
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: migrating ===========
main: -- change_column_default(:project_features, :model_registry_access_level, {:from=>20, :to=>nil})
main: -- change_column_default(:project_features, :model_experiments_access_level, {:from=>20, :to=>nil})
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: migrated (0.0182s) ==

Down

$ bundle exec rails db:migrate:down:main VERSION=20260617130000
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: reverting ===========
main: -- change_column_default(:project_features, :model_experiments_access_level, {:from=>nil, :to=>20})
main: -- change_column_default(:project_features, :model_registry_access_level, {:from=>nil, :to=>20})
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: reverted (0.0201s) ==

Unrelated change: trailing newlines on ai_agent schema migration markers

This MR also strips a trailing newline from four db/schema_migrations/ marker files unrelated to this change:

  • 20260616100000, 20260616100100, 20260616100200, 20260616100300 (the ai_agent table-drop migrations)

Why: those four markers were committed to master with a trailing newline, but db:schema:dump generates schema-migration markers without one. The db:check-migrations job regenerates every marker and fails on any difference, so it was failing on this MR (and would fail on any MR that adds a migration) on those four files — nothing to do with the project_features change here. Stripping the trailing newline makes the committed files match the generated format and unblocks the pipeline. Checksums are unchanged (newline-only diff).

Edited by Ian Anderson

Merge request reports

Loading
Loading