Skip to content

fix(ui): key Microsoft login scopes by node protocol so the broker requests Graph scopes (#2373 to stage) - #2374

Open
dylan-savage wants to merge 1 commit into
stagefrom
fix/stage-m365-login-scope-keys
Open

dylan-savage wants to merge 1 commit into
stagefrom
fix/stage-m365-login-scope-keys

Conversation

@dylan-savage

Copy link
Copy Markdown
Collaborator

Summary

  • port fix(ui): key Microsoft login scopes by node protocol so the broker requests Graph scopes #2373 to the current stage release line
  • The Microsoft login button looked up per-service scopes under excel, word, onedrive, outlook_mail, outlook_calendar, but formContext.provider is the node protocol name (tool_excel, …). The lookup always missed, so no scope= was sent and the broker requested identity scopes only (openid email profile offline_access).
  • Result on staging: login succeeds and stores a token, but every Microsoft 365 tool fails to start with authorization is missing required scopes … Missing: Files.ReadWrite.
  • Fix: key SERVICE_TIER_SCOPES by protocol name (matching LoginWithGoogleButton's tool_gmail). Scope values are unchanged. The map moves to module scope (exported) so it can be tested.

Type

fix (stage port)

Testing

  • Tests added or updated. New LoginWithMicrosoftButton.test.ts reads every tool_microsoft_365/services.*.json and asserts the map has non-empty scopes for each protocol and every <prefix>.access tier. It fails on the old keys (no entry for provider 'tool_excel').
  • Tested locally. After the fix, the button's broker URL carries scope=Files.ReadWrite. A hand-built broker URL with that scope got the Files permissions on Microsoft's consent screen, where before only identity permissions were requested.
  • ./builder test not run locally; GitHub CI is the repository-wide gate
LoginWithMicrosoftButton.test.ts: 2 passed
apps/shared suite (shared:test set, on stage base): 121 passed, 0 failed
tsc --noEmit -p apps/shared/tsconfig.json: clean
prettier --check: clean; eslint: 0 errors (2 pre-existing warnings, unchanged)

Existing users must reconnect after this ships. Tokens issued before the fix carry no Graph scopes.

Checklist

  • Commit messages follow conventional commits
  • No secrets or credentials included
  • Wiki updated (not applicable)
  • Breaking changes documented (not applicable; no breaking changes)

Paired PR

Develop: #2373

Release Coordination

Merge with the release owner so the SaaS engine pin advances in the same release window (current saas pin b7764b8 has this bug).

Linked Issue

No issue filed. Found during staging release testing.

🤖 Generated with Claude Code

…quests Graph scopes

LoginWithMicrosoftButton looked up SERVICE_TIER_SCOPES[formContext.provider],
but formContext.provider is the node protocol name (tool_excel, tool_word,
tool_onedrive, tool_outlook_mail, tool_outlook_calendar) while the map was
keyed excel/word/onedrive/outlook_mail/outlook_calendar. Every lookup missed,
so no scope param was sent, the broker requested identity only, and the node
rejected the token (e.g. "Missing: Files.ReadWrite").

Rekey the map by protocol name (scope values unchanged; they mirror
core/microsoft_access.py) and hoist it to an exported module constant. Add a
test that reads every tool_microsoft_365 services.*.json and asserts the map
has non-empty scopes for each protocol and each access tier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • develop
  • release/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: rocketride-org/rocketride-server/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 75d07513-e8ed-4d1c-881c-f4c00f456179

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dylan-savage - good catch, and the test is the right shape: it reads the real services.*.json from the node, it fails on the old keys, and it will fail again the day a new Microsoft 365 service ships without scopes. I checked all five protocols against nodes/src/nodes/core/microsoft_access.py and the map mirrors the AccessSpecs exactly, User.ReadBasic.All included.

Approve

Two non-blocking notes inline; neither needs to hold the merge. This is the identical change as #2373 against stage - both files match byte for byte, so the same notes apply there.

One item has no file or line, so it goes here. The description says existing users must reconnect after this ships, and I cannot find a way for them to do that in the product. The button is disabled={authenticated} (LoginWithMicrosoftButton.tsx:178), and microsoft.userToken is "ui:disabled": true in nodes/src/nodes/core/services.common.microsoft.json, so somebody holding a pre-fix token can neither press Login again nor clear the token. Deleting and recreating the node looks like the only route. Could you confirm? If that is the answer it belongs in the release note, because their tools keep failing until they do it.

for (const { file, tiers } of services) assert.ok(tiers.length > 0, `${file} has no <prefix>.access enum`);
});

test('scope map is keyed by node protocol with scopes for every access tier', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should fix - the same gap is open on the Google button, for four services out of five.

LoginWithGoogleButton.tsx still keeps its SERVICE_TIER_SCOPES inline inside handleHybridSignIn, and that map has exactly one key: tool_gmail.

nodes/src/nodes/tool_google_workspace/ ships five services. Every one of them has an access enum and renders GoogleButtonWidget under google.authType: user:

services file protocol in the map
services.gmail.json tool_gmail yes
services.calendar.json tool_calendar no
services.docs.json tool_docs no
services.drive.json tool_drive no
services.sheets.json tool_sheets no

So four of them send no scope= and fall back to what the comment calls the broker's legacy default consent - the same branch that turned out to grant identity scopes only on the Microsoft side, which is the bug this PR is fixing. Whether Google's legacy default happens to cover those four is the question, and it is worth answering rather than assuming: that assumption is what shipped here.

The cheap move is to export the Google map the way you exported this one, and run this test over both - reading services.*.json from tool_google_workspace for the Google half. If the gap is real the test says so immediately. If the broker's default does cover them, a comment saying so stops the next person asking.

* offline_access + identity scopes are appended by the broker, matching the
* Google flow.
*/
export const SERVICE_TIER_SCOPES: Record<string, Record<string, string[]>> = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - the test pins the keys, the scope values can still drift.

This test proves every provider and every tier has an entry. It does not check which scopes, and these values are a hand-kept copy of the AccessSpecs in nodes/src/nodes/core/microsoft_access.py. They match today - I compared all five, including the User.ReadBasic.All that only the widget requests.

If someone raises a tier on the Python side, say word.write to Files.ReadWrite.All, this map keeps requesting the old scope and the symptom is the one you just fixed: consent succeeds, the tool fails at invoke time with "Missing: ...". Nothing goes red on the way there.

How far to take this is your call, and it is a design question rather than a change to make in this PR. The cheap version is a pointer comment in microsoft_access.py saying a second copy lives in this file. The real version is one source that both sides read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants