Update tag policy to take into account protection rules

Context

We have an existing permission type for ContainerRepositoryTag and we need to override this to take into account tag protection rules.

Ideally, we want to throw in an additional validation (protection rules) on top of the permissions system. I have a sense that this should be possible with some override functions, similar to overriding an object function from a resolver to add more logic.

One usage of this is for displaying / hiding the checkbox for deleting tags: https://gitlab.com/gitlab-org/gitlab/-/issues/499872/designs/protected-tag-identifier.png.

What does this MR do?

  • Add a new rule to ContainerRegistry::TagPolicy to prevent destroy_container_image ability if the tag is protected for delete.
  • Centralize the logic of Tag#protection_rule in the ContainerRegistry::Tag class, so that it can be called from Types::ContainerRegistry::ContainerRepositoryTagType & ContainerRegistry::TagPolicy.
  • Add the needed specs.

References

Please include cross links to any resources that are relevant to this MR. This will give reviewers and future readers helpful context to give an efficient review of the changes introduced.

MR acceptance checklist

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

Screenshots or screen recordings

N/A

How to set up and validate locally

  1. Prepare a project with several container registry tags on it
  2. Create several tag protection rules for the project. Create some rule that matches the registry tags and some that don't.
project = Project.find(id)

# will not match a tag, unless that tag has the name `thiswillnotmatch`
project.container_registry_protection_tag_rules.create(tag_name_pattern: "thiswillnotmatch", minimum_access_level_for_push: "maintainer", minimum_access_level_for_delete: "maintainer")

# will always match a tag
project.container_registry_protection_tag_rules.create(tag_name_pattern: ".*", minimum_access_level_for_push: "maintainer", minimum_access_level_for_delete: "owner")

# another rule that matches the tag, update `name` to the tag name to make sure it matches
project.container_registry_protection_tag_rules.create(tag_name_pattern: "name", minimum_access_level_for_push: "owner", minimum_access_level_for_delete: "maintainer")
  1. Enable the container_registry_protected_tags feature flag:
Feature.enable(:container_registry_protected_tags)
  1. Login as a root user and visit http://gdk.test:3000/-/graphql-explorer and send a GraphQL query. The response should contain userPermissions field for each tag.
{
  containerRepository(id: "gid://gitlab/ContainerRepository/<Container Repository ID>") {
    id
    tagsCount
    tags(first: 5) {
      nodes {
        userPermissions {
          destroyContainerRepositoryTag
        }
      }
    }
  }
}

Since the root user is admin, destroyContainerRepositoryTag should be true.

  1. Impersonate a different member in the project with a maintainer role and visit http://gdk.test:3000/-/graphql-explorer again and repeat the same query.

    This time, the value of destroyContainerRepositoryTag should be false, because the protection rule that matches the tag has minimum_access_level_for_delete: "owner".

  2. You can play around by trying different users with different roles and by updating minimum_access_level_for_delete for the matching tag's rule.

Related to #512367

Edited by Moaz Khalifa

Merge request reports

Loading