Authorize reading CustomEmoji for granular tokens

What does this MR do and why?

CustomEmoji declared no granular token directive, so fine-grained personal access tokens got an empty customEmoji connection: count was right but nodes was empty, without any error. Tools that sync emojis with such a token then saw none and tried to recreate every emoji, which failed on the existing names.

Add a read_custom_emoji permission on the group boundary, which pairs with the existing create and delete permissions.

Screenshots

Before After
image Screenshot_From_2026-10-08_06-31-37

References

How to set up and validate locally

Prerequisites: a group (my-group) with at least one custom emoji; a fine-grained PAT scoped to that group with Group: Read and Custom Emoji: Create (plus Custom Emoji: Read once this MR is applied); and a classic PAT with api or read_api for comparison.

Example:

List custom emoji with a classic vs. a fine-grained token.

Run the same query once with each token, $CLASSIC_TOKEN and $FINE_GRAINED_TOKEN:

curl --request POST 'https://gitlab.example.com/api/graphql' \
  --header "Authorization: Bearer $CLASSIC_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{"query": "{ group(fullPath: \"my-group\") { customEmoji { count nodes { id name } } } }"}'

Classic token (output shortened):

{"data":{"group":{"customEmoji":{"count":39,"nodes":[{"id":"gid://gitlab/CustomEmoji/303","name":"awesome"}, ...]}}}}

Fine-grained token (count matches, nodes is empty, no error):

{"data":{"group":{"customEmoji":{"count":39,"nodes":[]}}}}

With this MR

  1. Add Custom Emoji: Read to the fine-grained token (the permission appears next to Custom Emoji: Create and Custom Emoji: Delete on the group boundary).
  2. Rerun Example 1 with $FINE_GRAINED_TOKEN. nodes now matches the classic token's output.
  3. Without Custom Emoji: Read, nodes stays empty.

Set up with Caproni

This shows how to try the MR in Caproni without a full image build. You layer the MR's changed files on top of the webservice image the cluster already runs, load the result into the cluster, and point the chart at it. Only the webservice needs the change, because it serves GraphQL.

Run the docker and git commands from a gitlab-org/gitlab checkout of this MR's branch, and the caproni commands from the gitlab-caproni directory. Only four files matter at runtime:

  • app/graphql/types/custom_emoji_type.rb
  • app/graphql/types/permission_types/custom_emoji.rb
  • config/authz/permissions/custom_emoji/read.yml
  • config/authz/permission_groups/assignable_permissions/groups/custom_emoji/read.yml

This was verified on a cluster running gitlab-webservice-ee:v19.4.1. The files apply unchanged on top of 19.4.1.

  1. Find the image the cluster runs:

    caproni kubectl -n gitlab get deploy gitlab-webservice-default \
      -o jsonpath='{.spec.template.spec.containers[0].image}'
    # registry.gitlab.com/gitlab-org/build/cng/gitlab-webservice-ee:v19.4.1
  2. Build an image with the changed files on top of it (from the gitlab checkout):

    BASE=registry.gitlab.com/gitlab-org/build/cng/gitlab-webservice-ee:v19.4.1
    CTX=$(mktemp -d)
    git ls-files -z -- \
      app/graphql/types/custom_emoji_type.rb \
      app/graphql/types/permission_types/custom_emoji.rb \
      config/authz/permissions/custom_emoji/read.yml \
      config/authz/permission_groups/assignable_permissions/groups/custom_emoji/read.yml \
      | xargs -0 cp --parents -t "$CTX"
    printf 'FROM %s\nCOPY --chown=git:git . /srv/gitlab/\n' "$BASE" \
      | docker build -t "$BASE-emoji" -f - "$CTX"
  3. Load it into the cluster:

    docker save "$BASE-emoji" | caproni image load
  4. In caproni.local.yaml, comment out edit_reloaders (in production mode, plain caproni up, no reloader may be in edit mode) and add the following. The tag must not be latest: the chart uses imagePullPolicy: IfNotPresent, so a loaded non-latest tag is used without pulling.

    deployers:
      gitlab:
        helm:
          dynamic_overrides:
            - type: inline
              value_map:
                gitlab.webservice.image.tag: v19.4.1-emoji
  5. Deploy and check that the new image is running:

    caproni up
    caproni kubectl -n gitlab get deploy gitlab-webservice-default \
      -o jsonpath='{.spec.template.spec.containers[0].image}'
    # registry.gitlab.com/gitlab-org/build/cng/gitlab-webservice-ee:v19.4.1-emoji
  6. In http://gitlab.caproni.test, create a classic PAT and a fine-grained PAT scoped to the group with Group: Read and Custom Emoji: Read. Then run the example above against http://gitlab.caproni.test instead of https://gitlab.example.com.

  7. To undo, remove the override, restore edit_reloaders in caproni.local.yaml, and run caproni up.

Alternatively, edit mode (caproni run) runs the local checkout directly and needs no image. It uses the gitlabhq_development database while the cluster pods use gitlabhq_production, so tokens created in one mode do not exist in the other.

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 Peter Leitzen

Merge request reports

Loading
Loading