Add role-based permissions to AI instance and namespace settings table

What does this MR do and why?

This change adds role-based permission controls to AI settings in GitLab. It introduces three new permission levels that determine who can execute AI features, manage AI settings, and enable AI on projects. The system now allows administrators to set minimum access levels (e.g., maintainer, developer) for different AI operations, giving organizations greater control over who can use and configure AI features.

The changes include database updates to store these permission settings and validation rules to ensure only users with a specific access level or higher can manage, execute, or enable AI features.

References

Database

Migration up
➜  gitlab git:(lw/578551-ai-settings-role-permissions) ✗ rails db:migrate
main: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127452
main: == 20241112145748 AddRolePermissionsToAiSettings: migrating ===================
main: -- add_column(:ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
main:    -> 0.0162s
main: -- add_column(:ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
main:    -> 0.0040s
main: -- add_column(:ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
main:    -> 0.0035s
main: == 20241112145748 AddRolePermissionsToAiSettings: migrated (0.0655s) ==========

main: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127452
ci: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127453
ci: == 20241112145748 AddRolePermissionsToAiSettings: migrating ===================
ci: -- add_column(:ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0201s
ci: -- add_column(:ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0032s
ci: -- add_column(:ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0027s
ci: == 20241112145748 AddRolePermissionsToAiSettings: migrated (0.0397s) ==========

ci: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127453
main: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127455
main: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: migrating ==========
main: -- add_column(:namespace_ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
main:    -> 0.0035s
main: -- add_column(:namespace_ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
main:    -> 0.0024s
main: -- add_column(:namespace_ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
main:    -> 0.0022s
main: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: migrated (0.0201s) =

main: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127455
ci: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127459
ci: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: migrating ==========
ci: -- add_column(:namespace_ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0037s
ci: -- add_column(:namespace_ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0101s
ci: -- add_column(:namespace_ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0025s
ci: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: migrated (0.0337s) =

ci: == [advisory_lock_connection] object_id: 130920, pg_backend_pid: 127459
Migration down
➜  gitlab git:(lw/578551-ai-settings-role-permissions) rails db:migrate:down:main db:migrate:down:ci VERSION=20241112150537
main: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 126840
main: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: reverting ==========
main: -- remove_column(:namespace_ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
main:    -> 0.0089s
main: -- remove_column(:namespace_ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
main:    -> 0.0025s
main: -- remove_column(:namespace_ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
main:    -> 0.0025s
main: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: reverted (0.0806s) =

main: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 126840
ci: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 126914
ci: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: reverting ==========
ci: -- remove_column(:namespace_ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0052s
ci: -- remove_column(:namespace_ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0048s
ci: -- remove_column(:namespace_ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0041s
ci: == 20241112150537 AddRolePermissionsToNamespaceAiSettings: reverted (0.0332s) =

ci: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 126914
➜  gitlab git:(lw/578551-ai-settings-role-permissions) ✗ rails db:migrate:down:main db:migrate:down:ci VERSION=20241112145748
main: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 127178
main: == 20241112145748 AddRolePermissionsToAiSettings: reverting ===================
main: -- remove_column(:ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
main:    -> 0.0111s
main: -- remove_column(:ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
main:    -> 0.0590s
main: -- remove_column(:ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
main:    -> 0.0032s
main: == 20241112145748 AddRolePermissionsToAiSettings: reverted (0.1095s) ==========

main: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 127178
ci: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 127209
ci: == 20241112145748 AddRolePermissionsToAiSettings: reverting ===================
ci: -- remove_column(:ai_settings, :minimum_access_level_enable_on_projects, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0049s
ci: -- remove_column(:ai_settings, :minimum_access_level_manage, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0048s
ci: -- remove_column(:ai_settings, :minimum_access_level_execute, :integer, {:limit=>2, :null=>true})
ci:    -> 0.0042s
ci: == 20241112145748 AddRolePermissionsToAiSettings: reverted (0.0356s) ==========

ci: == [advisory_lock_connection] object_id: 130620, pg_backend_pid: 127209

Screenshots or screen recordings

Before After

How to set up and validate locally

Green test pipeline

Manual verification

[7] pry(main)> i = Ai::Setting.instance
=> #<Ai::Setting:0x00007f971ef7f6e0
 id: 1,
 ai_gateway_url: "https://example.com/site",
 singleton: true,
 created_at: Wed, 04 Dec 2024 10:59:19.842623000 UTC +00:00,
 updated_at: Tue, 19 Aug 2025 19:54:16.233578000 UTC +00:00,
 amazon_q_oauth_application_id: nil,
 amazon_q_service_account_user_id: nil,
 amazon_q_ready: false,
 amazon_q_role_arn: "arn:aws:iam::145023121044:role/NewRole",
 duo_workflow_service_account_user_id: 97,
 duo_workflow_oauth_application_id: 16,
 enabled_instance_verbose_ai_logs: false,
 duo_core_features_enabled: true,
 duo_agent_platform_service_url: nil,
 minimum_access_level_execute: nil,
 minimum_access_level_manage: nil,
 minimum_access_level_enable_on_projects: nil>
[8] pry(main)> i.valid?
  Ai::Setting Count (0.5ms)  SELECT COUNT(*) FROM "ai_settings" 
=> true

[9] pry(main)> f = Ai::NamespaceSetting.first
=> #<Ai::NamespaceSetting:0x00007f971e58d560
 namespace_id: 201,
 duo_workflow_mcp_enabled: false,
 minimum_access_level_execute: nil,
 minimum_access_level_manage: nil,
 minimum_access_level_enable_on_projects: nil>
[10] pry(main)> f.valid?
=> true
[11] pry(main)> f.attributes = { minimum_access_level_execute: 30, minimum_access_level_manage: 30, minimum_access_level_enable_on_projects: 30 }
=> {:minimum_access_level_execute=>30, :minimum_access_level_manage=>30, :minimum_access_level_enable_on_projects=>30}
[12] pry(main)> f.valid?
=> false
[13] pry(main)> f.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=minimum_access_level_manage, type=greater_than_or_equal_to, options={:allow_nil=>true, :value=>30, :count=>40}>, #<ActiveModel::Error attribute=minimum_access_level_enable_on_projects, type=greater_than_or_equal_to, options={:allow_nil=>true, :value=>30, :count=>40}>]>
[14] pry(main)> f.attributes = { minimum_access_level_execute: 40, minimum_access_level_manage: 40, minimum_access_level_enable_on_projects: 40 }
=> {:minimum_access_level_execute=>40, :minimum_access_level_manage=>40, :minimum_access_level_enable_on_projects=>40}
[15] pry(main)> f.valid?
=> true

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Lukas Wanko

Merge request reports

Loading