Skip to content

MGMT-20908: Forward authorization#4

Merged
eranco74 merged 1 commit intoopenshift-assisted:masterfrom
jhernand:forward_authorization
Jun 25, 2025
Merged

MGMT-20908: Forward authorization#4
eranco74 merged 1 commit intoopenshift-assisted:masterfrom
jhernand:forward_authorization

Conversation

@jhernand
Copy link
Copy Markdown
Collaborator

@jhernand jhernand commented Jun 25, 2025

This patch changes the MCP server so that if the Authorization header contains a bearer token it will forward it to the assisted service REST API.

To existing behaviour to get the use the offline token from the environment variable OFFLINE_TOKEN is preserved when there is no Authorization header in the request.

Note that in order to test this it is necessary to have a MCP client that is able to send the Authorization header. I did so making a small change to the mcp command line tool (from the https://github.com/f/mcptools project) so that it reads the token from a TOKEN environment variable:

diff --git a/cmd/mcptools/commands/utils.go b/cmd/mcptools/commands/utils.go
index f3a9be0..19918f2 100644
--- a/cmd/mcptools/commands/utils.go
+++ b/cmd/mcptools/commands/utils.go
@@ -5,6 +5,7 @@ import (
        "context"
        "encoding/json"
        "fmt"
+       "os"
        "strings"
        "time"

@@ -47,7 +48,13 @@ var CreateClientFunc = func(args []string, _ ...client.ClientOption) (*client.Cl
        var err error

        if len(args) == 1 && IsHTTP(args[0]) {
-               c, err = client.NewSSEMCPClient(args[0])
+               token := os.Getenv("TOKEN")
+               c, err = client.NewSSEMCPClient(
+                       args[0],
+                       client.WithHeaders(map[string]string{
+                               "Authorization": fmt.Sprintf("Bearer %s", token),
+                       }),
+               )
                if err != nil {
                        return nil, err
                }

Then I can put the access token in that environment variable, and call a tool. For example, start the server without the offline token:

$ unset OFFLINE_TOKEN
$ uv run server.py

Then get the token and call some tool:

$ ocm login --use-auth-code
$ TOKEN=$(ocm token) mcp call list_clusters http://localhost:8000/sse | jq
[
  {
    "name": "sno",
    "id": "aef5bb4e-397d-4e21-a776-5f1405b2cf10",
    "openshift_version": "4.19.0",
    "status": "pending-for-input"
  }
]

Related: https://issues.redhat.com/browse/MGMT-20908

Summary by CodeRabbit

  • New Features

    • Improved authentication by supporting access tokens from request headers or via token refresh, enhancing flexibility for users.
  • Refactor

    • Streamlined token handling for inventory operations, simplifying the authentication process and reducing reliance on offline tokens.

This patch changes the MCP server so that if the `Authorization` header
contains a bearer token it will forward it to the assisted service REST
API.

To existing behaviour to get the use the offline token from the
environment variable `OFFLINE_TOKEN` is preserved when there is no
`Authorization` header in the request.

Note that in order to test this it is necessary to have a MCP client
that is able to send the `Authorization` header. I did so making a small
change to the `mcp` command line tool (from the
https://github.com/f/mcptools project) so that it reads the token from a
`TOKEN` environment variable:

```diff
diff --git a/cmd/mcptools/commands/utils.go b/cmd/mcptools/commands/utils.go
index f3a9be0..19918f2 100644
--- a/cmd/mcptools/commands/utils.go
+++ b/cmd/mcptools/commands/utils.go
@@ -5,6 +5,7 @@ import (
        "context"
        "encoding/json"
        "fmt"
+       "os"
        "strings"
        "time"

@@ -47,7 +48,13 @@ var CreateClientFunc = func(args []string, _ ...client.ClientOption) (*client.Cl
        var err error

        if len(args) == 1 && IsHTTP(args[0]) {
-               c, err = client.NewSSEMCPClient(args[0])
+               token := os.Getenv("TOKEN")
+               c, err = client.NewSSEMCPClient(
+                       args[0],
+                       client.WithHeaders(map[string]string{
+                               "Authorization": fmt.Sprintf("Bearer %s", token),
+                       }),
+               )
                if err != nil {
                        return nil, err
                }
```

Then I can put the access token in that environment variable, and call a
tool. For example, start the server without the offline token:

```shell
$ unset OFFLINE_TOKEN
$ uv run server.py
```

Then get the token and call some tool:

```shell
$ ocm login --use-auth-code
$ TOKEN=$(ocm token) mcp call list_clusters http://localhost:8000/sse | jq
[
  {
    "name": "sno",
    "id": "aef5bb4e-397d-4e21-a776-5f1405b2cf10",
    "openshift_version": "4.19.0",
    "status": "pending-for-input"
  }
]
```

Related: https://issues.redhat.com/browse/MGMT-20908
Signed-off-by: Juan Hernandez <juan.hernandez@redhat.com>
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jun 25, 2025
@openshift-ci-robot
Copy link
Copy Markdown

openshift-ci-robot commented Jun 25, 2025

@jhernand: This pull request references MGMT-20908 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "4.20.0" version, but no target version was set.

Details

In response to this:

This patch changes the MCP server so that if the Authorization header contains a bearer token it will forward it to the assisted service REST API.

To existing behaviour to get the use the offline token from the environment variable OFFLINE_TOKEN is preserved when there is no Authorization header in the request.

Note that in order to test this it is necessary to have a MCP client that is able to send the Authorization header. I did so making a small change to the mcp command line tool (from the https://github.com/f/mcptools project) so that it reads the token from a TOKEN environment variable:

diff --git a/cmd/mcptools/commands/utils.go b/cmd/mcptools/commands/utils.go
index f3a9be0..19918f2 100644
--- a/cmd/mcptools/commands/utils.go
+++ b/cmd/mcptools/commands/utils.go
@@ -5,6 +5,7 @@ import (
       "context"
       "encoding/json"
       "fmt"
+       "os"
       "strings"
       "time"

@@ -47,7 +48,13 @@ var CreateClientFunc = func(args []string, _ ...client.ClientOption) (*client.Cl
       var err error

       if len(args) == 1 && IsHTTP(args[0]) {
-               c, err = client.NewSSEMCPClient(args[0])
+               token := os.Getenv("TOKEN")
+               c, err = client.NewSSEMCPClient(
+                       args[0],
+                       client.WithHeaders(map[string]string{
+                               "Authorization": fmt.Sprintf("Bearer %s", token),
+                       }),
+               )
               if err != nil {
                       return nil, err
               }

Then I can put the access token in that environment variable, and call a tool. For example, start the server without the offline token:

$ unset OFFLINE_TOKEN
$ uv run server.py

Then get the token and call some tool:

$ ocm login --use-auth-code
$ TOKEN=$(ocm token) mcp call list_clusters http://localhost:8000/sse | jq
[
 {
   "name": "sno",
   "id": "aef5bb4e-397d-4e21-a776-5f1405b2cf10",
   "openshift_version": "4.19.0",
   "status": "pending-for-input"
 }
]

Related: https://issues.redhat.com/browse/MGMT-20908

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Jun 25, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: jhernand

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Jun 25, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 25, 2025

Walkthrough

The changes implement a new token retrieval mechanism by introducing a get_access_token() function that obtains an access token either from the request header or by refreshing an offline token. The InventoryClient class is refactored to require an access token directly, removing previous logic for exchanging offline tokens.

Changes

File(s) Change Summary
server.py Added get_access_token() to centralize access token retrieval; updated all InventoryClient calls to use it; imported requests.
service_client/assisted_service_api.py Refactored InventoryClient to accept access_token directly; removed offline token exchange logic; updated pull secret retrieval.

Poem

In the warren, tokens hop anew,
From headers or refresh, we know what to do!
InventoryClient now takes its key,
No more exchanges—just simplicity.
With paws on code, we leap ahead,
Secure and nimble, as rabbits are bred! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@openshift-ci-robot
Copy link
Copy Markdown

openshift-ci-robot commented Jun 25, 2025

@jhernand: This pull request references MGMT-20908 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "4.20.0" version, but no target version was set.

Details

In response to this:

This patch changes the MCP server so that if the Authorization header contains a bearer token it will forward it to the assisted service REST API.

To existing behaviour to get the use the offline token from the environment variable OFFLINE_TOKEN is preserved when there is no Authorization header in the request.

Note that in order to test this it is necessary to have a MCP client that is able to send the Authorization header. I did so making a small change to the mcp command line tool (from the https://github.com/f/mcptools project) so that it reads the token from a TOKEN environment variable:

diff --git a/cmd/mcptools/commands/utils.go b/cmd/mcptools/commands/utils.go
index f3a9be0..19918f2 100644
--- a/cmd/mcptools/commands/utils.go
+++ b/cmd/mcptools/commands/utils.go
@@ -5,6 +5,7 @@ import (
       "context"
       "encoding/json"
       "fmt"
+       "os"
       "strings"
       "time"

@@ -47,7 +48,13 @@ var CreateClientFunc = func(args []string, _ ...client.ClientOption) (*client.Cl
       var err error

       if len(args) == 1 && IsHTTP(args[0]) {
-               c, err = client.NewSSEMCPClient(args[0])
+               token := os.Getenv("TOKEN")
+               c, err = client.NewSSEMCPClient(
+                       args[0],
+                       client.WithHeaders(map[string]string{
+                               "Authorization": fmt.Sprintf("Bearer %s", token),
+                       }),
+               )
               if err != nil {
                       return nil, err
               }

Then I can put the access token in that environment variable, and call a tool. For example, start the server without the offline token:

$ unset OFFLINE_TOKEN
$ uv run server.py

Then get the token and call some tool:

$ ocm login --use-auth-code
$ TOKEN=$(ocm token) mcp call list_clusters http://localhost:8000/sse | jq
[
 {
   "name": "sno",
   "id": "aef5bb4e-397d-4e21-a776-5f1405b2cf10",
   "openshift_version": "4.19.0",
   "status": "pending-for-input"
 }
]

Related: https://issues.redhat.com/browse/MGMT-20908

Summary by CodeRabbit

  • New Features

  • Improved authentication by supporting access tokens from request headers or via token refresh, enhancing flexibility for users.

  • Refactor

  • Streamlined token handling for inventory operations, simplifying the authentication process and reducing reliance on offline tokens.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
server.py (2)

10-10: Fix formatting: Add required blank lines before function definitions.

Static analysis tools report missing blank lines before function definitions.

+
 def get_offline_token() -> str:
+
 def get_access_token() -> str:

Also applies to: 35-35


35-66: Review the token extraction and refresh logic for security and correctness.

The implementation looks good overall but has a few considerations:

  1. Security: The bearer token extraction correctly validates the Authorization header format
  2. Fallback logic: Properly falls back to offline token refresh when no bearer token is present
  3. Error handling: Uses raise_for_status() to handle HTTP errors appropriately

However, there are some areas for improvement:

Consider these enhancements:

 def get_access_token() -> str:
     """Retrieve the access token.

     This function tries to get the Red Hat OpenShift Cluster Manager (OCM) access token. First
     it tries to extract it from the authorization header, and if it isn't there then it tries
     to generate a new one using the offline token.

     Returns:
         str: The access token.

     Raises:
         RuntimeError: If it isn't possible to obtain or generate the access token.
     """
     # First try to get the token from the authorization header:
     request = mcp.get_context().request_context.request
     if request is not None:
         header = request.headers.get("Authorization")
         if header is not None:
             parts = header.split()
-            if len(parts) == 2 and parts[0].lower() == "bearer":
+            if len(parts) == 2 and parts[0].lower() == "bearer" and parts[1].strip():
                 return parts[1]

     # Now try to get the offline token, and generate a new access token from it:
     params = {
         "client_id": "cloud-services",
         "grant_type": "refresh_token",
         "refresh_token": get_offline_token(),
     }
     sso_url = os.environ.get("SSO_URL", "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token")
-    response = requests.post(sso_url, data=params)
+    response = requests.post(sso_url, data=params, timeout=30)
     response.raise_for_status()
     return response.json()["access_token"]

Suggested improvements:

  • Add validation to ensure the bearer token is not empty
  • Add timeout to the HTTP request to prevent hanging
service_client/assisted_service_api.py (1)

12-23: Consider adding token validation during initialization.

While the current implementation is functional, consider adding basic token validation to fail fast if an invalid token is provided:

 def __init__(self, access_token: str):
+    if not access_token or not access_token.strip():
+        raise ValueError("Access token cannot be empty")
     self.access_token = access_token
     self.pull_secret = self._get_pull_secret()
     self.inventory_url = os.environ.get("INVENTORY_URL", "https://api.openshift.com/api/assisted-install/v2")
     self.client_debug = os.environ.get("CLIENT_DEBUG", "False").lower() == "true"

This would help catch token-related issues early in the initialization process rather than during the first API call.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 338e98d and 2712d3c.

📒 Files selected for processing (2)
  • server.py (14 hunks)
  • service_client/assisted_service_api.py (1 hunks)
🧰 Additional context used
🪛 Flake8 (7.2.0)
server.py

[error] 10-10: expected 2 blank lines, found 1

(E302)


[error] 35-35: expected 2 blank lines, found 1

(E302)

🔇 Additional comments (4)
server.py (2)

4-4: LGTM: Required import for OAuth2 token refresh.

The requests library import is correctly added to support the HTTP POST call for token generation in the get_access_token() function.


86-86: LGTM: Consistent update to use access tokens.

All InventoryClient instantiations have been correctly updated to use get_access_token() instead of the previous offline token approach. This ensures consistent token handling across all API operations.

Also applies to: 104-104, 123-123, 140-140, 161-161, 185-185, 209-209, 232-232, 246-246, 260-260, 279-279, 300-300

service_client/assisted_service_api.py (2)

12-14: LGTM: Clean refactoring to accept access tokens directly.

The constructor change simplifies the token handling by accepting an access token directly instead of managing offline token exchange internally. The immediate call to _get_pull_secret() ensures the pull secret is available when needed.


18-23: LGTM: Simplified pull secret retrieval using access token.

The method correctly uses the instance's access token directly in the Authorization header. The error handling with raise_for_status() is appropriate for detecting authentication failures.

@eranco74
Copy link
Copy Markdown
Collaborator

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jun 25, 2025
@eranco74 eranco74 merged commit 33314d5 into openshift-assisted:master Jun 25, 2025
1 check passed
@jhernand jhernand deleted the forward_authorization branch June 26, 2025 19:18
@coderabbitai coderabbitai Bot mentioned this pull request Jul 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. lgtm Indicates that a PR is ready to be merged. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants