Implement update group secrets permission mutation

What does this MR do and why?

Related to #577341 (closed)

This MR implements the GraphQL mutation for updating/creating group-level secrets permissions. This allows group owners to grant users, roles, member roles, or groups the ability to manage secrets through the GitLab UI at the group level.

Important distinction: These permissions control who can manage secrets in the GitLab UI (create, read, update, delete secrets via the web interface). This is separate from CI/CD pipeline permissions that control which jobs can use secrets during execution.

This is the first part of work which implements the update mutation for group secrets permissions, following the same pattern established for project-level secrets permissions.

Technical Notes

Architecture & Design Decisions

1. Naming Convention: "Secrets" in Permission Names

The naming uses GroupSecretsPermission and ProjectSecretsPermission (instead of just GroupPermission or ProjectPermission) to clearly indicate that these permissions are specifically for controlling access to secrets within a group or project.

This distinction is important because:

  • Groups and projects may have many other types of OpenBao permissions down the road. We focus on secrets here for now.
  • These permissions specifically govern who can create, read, update, and delete secrets stored in the secrets manager through the GitLab UI

Also note the change to plural form of "Secrets". In some parts of the code, we still reference to a permission as secret_permission but I think it makes more sense to use the plural form because the permissions we are granting here applies to managing secrets as a whole and not a per-secret permission. We'll rename those references as we continue to build the other services and mutations.

2. Code Refactoring for Reusability

To support both project and group secrets permissions without duplication, this MR introduces several abstractions:

Model Layer (BaseSecretsPermission)

  • Extracted common validation and permission logic from SecretPermission into BaseSecretsPermission
  • Changed project attribute to generic resource attribute to support both projects and groups
  • Subclasses (ProjectSecretsPermission and GroupSecretsPermission) implement resource-specific validation logic
  • This allows most behavior to work the same way - code references resource or resource_id instead of project_id/group_id

Service Layer (SecretsPermissionsUpdateServiceHelpers)

  • Created a shared concern containing the core permission update logic
  • Both ProjectSecretsPermissions::UpdateService and GroupSecretsPermissions::UpdateService include this concern
  • Each service only needs to define:
    • resource - returns the project or group
    • client - returns the appropriate secrets manager client
    • permission_class - returns the appropriate permission model class

GraphQL Layer (SecretsPermissionInterface)

  • Used a Ruby concern (not a GraphQL interface) to share common fields between GroupSecretsPermissionType and ProjectSecretsPermissionType
  • Why a concern instead of GraphQL interface? There's no practical use case for polymorphic querying - group and project permissions are always queried in different scopes (group vs project context), so the simpler Ruby module approach is sufficient

PrincipalType Changes

  • Changed project_id to resource_id in the principal type resolver
  • This generalization allows the same PrincipalType to be used for both project and group permissions

3. Principal Eligibility Rules

Important: These rules determine which groups are eligible to be added as principals, not which groups automatically have access. Group owners must still explicitly grant permissions via the mutation.

The hierarchical validation checks provide flexibility for delegation:

For Group Permissions:

  • Same group can be added as a principal
  • Parent groups can be added (enables oversight - parent group members can manage child group secrets)
  • Child groups can be added (enables delegation - child group members can manage parent group secrets)
  • Groups that are shared with the target group can be added

For Project Permissions:

  • Groups that own the project can be added
  • Parent groups of the project's group can be added (enables oversight)
  • Child groups of the project's group can be added (enables delegation)
  • Groups that the project is explicitly shared with can be added

Why allow bidirectional eligibility? This provides owners with flexibility to delegate management both upward (parent groups managing child resources) and downward (child groups managing parent resources). Both project and group permissions follow the same bidirectional pattern for consistency.

4. Accepting group_path for Group Principals

When adding a Group as a principal, the mutation now accepts group_path (full path like my-org/sub-group) instead of just numeric id.

Why this change?

  • The frontend dropdown doesn't have a proper endpoint to fetch all groups that are eligible to be principals
  • Using group_path allows a simple text input where users manually enter the full group path
  • Validation happens on mutation submit

Migration strategy:

  • PrincipalInputType now accepts both id and group_path for backwards compatibility
  • Only Group-type principals use group_path; other principal types (User, Role, MemberRole) continue using id
  • The mutation resolver validates and converts group_path to id internally
  • Future project permissions mutations will only accept group_path for Group principals
  • After frontend migration, the id fallback will be removed

5. Authorization

New permission: configure_group_secrets_permission

  • Only available to group owners (not maintainers)
  • Consistent with configure_group_secrets_manager which is also owner-only
  • Maintainers get read_group_secrets_manager for read access
  • This matches the project-level pattern where owners configure and maintainers can read

Note: Project-level permissions mutations, queries, and services remain under the Permissions namespace for now. See "Follow-up Work" section below.

GraphQL Mutation

Example: Granting permission to a User

mutation {
  groupSecretsPermissionUpdate(input: {
    groupPath: "my-group"
    principal: {
      id: 123
      type: USER
    }
    permissions: ["read", "create", "update", "delete"]
    expiredAt: "2025-12-31"
  }) {
    secretsPermission {
      group {
        id
        fullPath
      }
      principal {
        id
        type
      }
      permissions
      grantedBy {
        id
        username
      }
      expiredAt
    }
    errors
  }
}

Example: Granting permission to a Group (using group_path)

mutation {
  groupSecretsPermissionUpdate(input: {
    groupPath: "my-group"
    principal: {
      groupPath: "my-org/sub-group"
      type: GROUP
    }
    permissions: ["read", "create", "update", "delete"]
  }) {
    secretsPermission {
      group {
        id
        fullPath
      }
      principal {
        id
        type
      }
      permissions
      grantedBy {
        id
        username
      }
      expiredAt
    }
    errors
  }
}

Feature Flag

This mutation is gated behind the existing group_secrets_manager feature flag.

Database Changes

None - permissions are stored in OpenBao, not the database.

Follow-up Work

Group Secrets Permissions

  • Implement list/query mutation for group secrets permissions
  • Implement delete mutation for group secrets permissions

Project Secrets Permissions Namespace Migration

Currently, project-level permissions mutations, queries, and services are under the Permissions namespace. To maintain consistency with the new GroupSecretsPermissions namespace, we need to migrate them to ProjectSecretsPermissions:

  1. Backend Migration MR: Migrate existing services to ProjectSecretsPermissions namespace

    • Move SecretsManagement::Permissions::ListServiceSecretsManagement::ProjectSecretsPermissions::ListService
    • Move SecretsManagement::Permissions::DeleteServiceSecretsManagement::ProjectSecretsPermissions::DeleteService
    • Update service references
  2. GraphQL Migration MR: Create new mutations/queries under ProjectSecretsPermissions namespace

    • Add new mutations: projectSecretsPermissionUpdate, projectSecretsPermissionDelete
    • Add new queries for listing project secrets permissions
    • New mutations will only accept group_path for Group principals (no id fallback)
    • Keep old mutations/queries (secretsPermissionUpdate, secretsPermissionDelete) for backward compatibility
    • Mark old mutations/queries as deprecated
  3. Frontend Migration: Update frontend to use new GraphQL mutations/queries with group_path

  4. Cleanup MR: Remove deprecated mutations/queries and id parameter fallback after frontend migration is complete

Edited by Erick Bajao

Merge request reports

Loading