Allow ai_workflows tokens to read award emoji
What does this MR do and why?
Composite identity tokens issued for Duo Agent Platform custom flows carry only the ai_workflows scope, which neither API surface accepted for award emoji. REST returned 403 insufficient_scope. GraphQL failed silently: Note.awardEmoji resolved to null with no entry in the errors array, so a client could not tell "reactions are unreadable" apart from "there are no reactions", and automation could treat a thumbs-downed comment as having received no feedback.
Declare the scope on the REST endpoints (read-only) and on the GraphQL field, its type, and that type's own fields. Policy checks are unchanged: AwardEmojiPolicy still delegates read_emoji to the awardable's read ability, so these tokens gain no visibility beyond what they are already permitted to see on the parent awardable or note.
The REST declaration is class-level and therefore also covers the group epic routes that EE appends. That stays within the existing boundary, because epics already allow ai_workflows access and so do epic notes.
References
How to set up and validate locally
-
In the Rails console, mint an OAuth token carrying only the
ai_workflowsscope. Use a non-admin user with Reporter or higher access to a private project:user = User.find_by_username!('<your-username>') organization = user.organization app = Authn::OauthApplication.create!( name: 'ai-workflows-award-emoji-check', redirect_uri: Gitlab::Routing.url_helpers.root_url, owner: user, organization: organization, scopes: 'ai_workflows', trusted: true, confidential: true ) token = OauthAccessToken.create!( application: app, resource_owner_id: user.id, organization: organization, scopes: [:ai_workflows], expires_in: 2.hours ) puts token.plaintext_token -
As that user, add a reaction to a note on a merge request in that project, and leave a second note without any reaction. Print both identifiers — REST takes the numeric ID, GraphQL takes the global ID:
[Note.find(<reacted_note_id>), Note.find(<plain_note_id>)].each do |note| puts [note.id, note.to_global_id].join(' ') end -
Read the reactions over REST:
curl --include \ --header "Authorization: Bearer <token>" \ --url "http://gdk.test:3000/api/v4/projects/<project_id>/merge_requests/<mr_iid>/notes/<note_id>/award_emoji"Expect
200 OKand the reaction.HEADon the same URL, andGETon.../award_emoji/<award_id>, also return200 OK. Before this MR, all return403 insufficient_scope. -
Read the reactions over GraphQL. Send the query to
/api/graphqlwith the sameAuthorization: Bearer <token>header — a signed-in GraphiQL session uses your session instead of the token, and validates nothing:query { note(id: "<note_global_id>") { id awardEmoji(first: 100) { nodes { name user { username } } } } }Expect the reaction nodes, and
{ "awardEmoji": { "nodes": [] } }for the note without reactions. Before this MR,awardEmojiisnullwith no entry inerrors— which is what makes the failure indistinguishable from "this note has no reactions". -
Verify the scope stays read-only.
POSTandDELETEagainst the same endpoints still return403 insufficient_scope, and the reaction list is unchanged afterwards. The error body now advertises"scope":"ai_workflows api read_api", so the write verbs are excluded by therequest.get? || request.head?condition rather than by the scope being absent.
All five steps were run against a local GDK on this branch, as a non-admin Reporter on a
private project. Group epic routes are covered by the same class-level declaration, but need a
Premium license, so they are left to the request specs in
ee/spec/requests/api/award_emoji_spec.rb.
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.