Introduce immutability to Protection::TagRule

What does this MR do and why?

In Container Registry: Tag immutability feature (&15139 - closed), we introduce immutable tag rules. These are rules used to protect tags such that they are immutable -- no one can push or delete them. Immutable tag rules have the same structure as our current protection rules just with the fields minimum_access_level_for_push and minimum_access_level_for_delete being nil since no one can push or delete them, regardless of the access level.

With this, we use the same model, Protection::TagRule but introduce immutability. We then see tag rules as protecting the tags for either 1) immutability or 2) mutability with access level restrictions.

In this MR, we:

  1. Update the tag rule model to introduce immutabiity rules.
  2. Update Tag#protection_rule method to return the immutable tag rule first, if it exists. Otherwise, fallback to looking at protection rules.
  3. Update protected_for_delete? to account for the scenario if user is nil (got this feedback from groupauthorization)
  4. Add tag_immutable_patterns to the hash that we return from the the JWT controller
  5. In GraphQL, return nil for protection if there is no applicable rule. Otherwise, we return the access level fields of the protection rule (highest among all immutable and mutable protection rules)

This MR is the first of several MRs from Container Registry: Tag immutability feature (&15139 - closed) that is updating the code around tag protection rules. Some of the next steps include:

We have done some prerequisites in #512442 (closed), #512733 (closed), and #512734 (closed) where we allowed the access level fields to be nullable in preparation for immutable tag rules.

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.

Validating locally

A. protection field on Tag

query {
  containerRepository(id: "gid://gitlab/ContainerRepository/227") {
    tags(first: 5) {
      nodes {
          name
          protection {
            minimumAccessLevelForPush
            minimumAccessLevelForDelete
          }
        }
      }
  }
}

In the following steps, we need a project that container repository tags. We also enable the feature: container_registry_protected_tags which some parts of immutable tags rely on. This flag is also currently being rolled out now in production so it would be cleaned up soon.

A.1 Starting with a project with no tag rules

protection would be null:

{
  "data": {
    "containerRepository": {
      "tags": {
        "nodes": [
          {
            "name": "tag1",
            "protection": null
           ...

A.2 Create a tag protection rule on the project with a tag_name_pattern matching the tag name

ContainerRegistry::Protection::TagRule.create(project_id: 28, tag_name_pattern: 'ta', minimum_access_level_for_push: 'owner', minimum_access_level_for_delete: 'maintainer')
{
  "data": {
    "containerRepository": {
      "tags": {
        "nodes": [
          {
            "name": "tag1",
            "protection": {
              "minimumAccessLevelForPush": "OWNER",
              "minimumAccessLevelForDelete": "MAINTAINER"
            }
          ...

A.3 Create a tag immutability protection rule on the project with a tag_name_pattern matching the tag name

ContainerRegistry::Protection::TagRule.create(project_id: 28, tag_name_pattern: 'tag', minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil)

# for this step, we want it disabled first.
Feature.disable :container_registry_immutable_tags

The result from A.2 should not change. The immutable tag is ignored.

A.3 Enable the feature flag

Feature.enable :container_registry_immutable_tags

We would see the access levels with null as the value as no access level can push or delete the tags.

{
  "data": {
    "containerRepository": {
      "tags": {
        "nodes": [
          {
            "name": "tag1",
            "protection": {
              "minimumAccessLevelForPush": null,
              "minimumAccessLevelForDelete": null
            }
        ...

B. tag_immutable_patterns on JWT

We'll assume that you have a gitlab-org/gitlab-test project in your GDK, and a PAT from a user with Owner role with read/write registry permissions. Set the CR_PAT environment variable to the PAT value and CR_USER to the username.

For these tests, the jq and jwt CLI tools are used to facilitate parsing the output of requests. Feel free to adjust the curl commands if you'd like to use other tools.

Prerequisites:

  1. Set the CR_USER and CR_PAT variables in your terminal
CR_USER=username
CR_PAT=glpatxxxx
  1. Create an immutable rule. Note that tag_name_pattern needs to be unique so replace it below if it's already taken in your local machine.
project = Project.find_by_full_path 'gitlab-org/gitlab-test'
project.container_registry_protection_tag_rules.create(tag_name_pattern: 'sample', minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil)

B.1 We test out the curl below when the feature flag for immutable tags is enabled:

 Feature.enable :container_registry_immutable_tags
curl -s -u "$CR_USER:$CR_PAT" \
    -G \
    --data-urlencode "service=container_registry" \
    --data-urlencode "scope=repository:gitlab-org/gitlab-test:pull,push,delete" \
    http://gdk.test:3000/jwt/auth | jq -r '.token' | jwt decode -j - | jq -r '.payload.access'

Result: The tag_name_pattern from our immutable rule appears in tag_immutable_patterns.

[
  {
    "actions": [
      "pull",
      "push",
      "delete"
    ],
    "meta": {
      "project_id": 2,
      "project_path": "gitlab-org/gitlab-test",
      "root_namespace_id": 24,
      "tag_immutable_patterns": [
        "sample"
      ]
    },
    "name": "gitlab-org/gitlab-test",
    "type": "repository"
  }
]

B.2 We can try it when the feature flag for immutable tags is disabled with the same curl command:

 Feature.disable :container_registry_immutable_tags

Result: tag_immutable_patterns is no longer part of the response. Note, I have experienced some caching sometimes that even after disabling the feature flag, tag_immutable_patterns still appears. If this happens, try to update an immutable rule and that will reset the cache.

[
  {
    "actions": [
      "pull",
      "push",
      "delete"
    ],
    "meta": {
      "project_id": 2,
      "project_path": "gitlab-org/gitlab-test",
      "root_namespace_id": 24,
    },
    "name": "gitlab-org/gitlab-test",
    "type": "repository"
  }
]

Related to #515996 (closed)

Edited by Adie (she/her)

Merge request reports

Loading