-
Notifications
You must be signed in to change notification settings - Fork 277
Two new tools: copy doc and insert template variables + support for service account credentials #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2a5167a
0ca973a
0925bac
c2ac2df
ef0bbd7
d1dbeea
f7745ad
81e46ff
9aac686
05627f9
4caf224
59a78d6
e91fe38
c4160c7
9092a53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "version": "0.2.0", | ||
| "configurations": [ | ||
| { | ||
| "name": "Run in Debug Mode", | ||
taylorwilsdon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "type": "debugpy", | ||
| "request": "launch", | ||
| "program": "${workspaceFolder}/main.py", | ||
| "args": [ | ||
| "--transport", | ||
| "streamable-http", | ||
| "--tools", | ||
| "docs", | ||
| "--single-user" | ||
| ], | ||
| "env": { | ||
| "WORKSPACE_MCP_PORT": "8000" | ||
| }, | ||
| "justMyCode": false, | ||
| "console": "integratedTerminal" | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "servers": { | ||
| "google_workspace": { | ||
| "type": "stdio", | ||
| "command": "uvx", | ||
| "args": ["workspace-mcp", "--single-user"] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,21 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # auth/google_auth.py | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List, Optional, Tuple, Dict, Any, Callable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Dict, List, Optional, Tuple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import jwt | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.auth.exceptions import RefreshError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.auth.transport.requests import Request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.oauth2 import service_account | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.oauth2.credentials import Credentials | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google_auth_oauthlib.flow import Flow, InstalledAppFlow | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.auth.transport.requests import Request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from google.auth.exceptions import RefreshError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from googleapiclient.discovery import build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from googleapiclient.errors import HttpError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from auth.scopes import OAUTH_STATE_TO_SESSION_ID_MAP, SCOPES | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Configure logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -43,6 +46,7 @@ def _find_any_credentials(base_dir: str = DEFAULT_CREDENTIALS_DIR) -> Optional[C | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Find and load any valid credentials from the credentials directory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Used in single-user mode to bypass session-to-OAuth mapping. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Supports both OAuth2 and service account credentials. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| First valid Credentials object found, or None if none exist. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -56,8 +60,24 @@ def _find_any_credentials(base_dir: str = DEFAULT_CREDENTIALS_DIR) -> Optional[C | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if filename.endswith('.json'): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filepath = os.path.join(base_dir, filename) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check if this is a service account file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "iam.gserviceaccount.com" in filename: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"[single-user] Found service account file: {filepath}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials = service_account.Credentials.from_service_account_file(filepath,scopes=SCOPES) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"[single-user] Successfully loaded service account credentials from {filepath}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Force refresh to get a token, since by default it's not set and the library considers then credentials to be invalid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials.refresh(Request()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return credentials | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning(f"[single-user] Error loading service account credentials from {filepath}: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+75
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "iam.gserviceaccount.com" in filename: | |
| logger.info(f"[single-user] Found service account file: {filepath}") | |
| try: | |
| credentials = service_account.Credentials.from_service_account_file(filepath,scopes=SCOPES) | |
| logger.info(f"[single-user] Successfully loaded service account credentials from {filepath}") | |
| # Force refresh to get a token, since by default it's not set and the library considers then credentials to be invalid | |
| credentials.refresh(Request()) | |
| return credentials | |
| except Exception as e: | |
| logger.warning(f"[single-user] Error loading service account credentials from {filepath}: {e}") | |
| continue | |
| try: | |
| with open(filepath, 'r') as f: | |
| creds_data = json.load(f) | |
| # Validate required keys for service account credentials | |
| if all(key in creds_data for key in ["client_email", "private_key", "project_id"]): | |
| logger.info(f"[single-user] Found valid service account file: {filepath}") | |
| try: | |
| credentials = service_account.Credentials.from_service_account_file(filepath, scopes=SCOPES) | |
| logger.info(f"[single-user] Successfully loaded service account credentials from {filepath}") | |
| # Force refresh to get a token, since by default it's not set and the library considers the credentials to be invalid | |
| credentials.refresh(Request()) | |
| return credentials | |
| except Exception as e: | |
| logger.warning(f"[single-user] Error loading service account credentials from {filepath}: {e}") | |
| continue | |
| else: | |
| logger.warning(f"[single-user] File {filepath} does not contain valid service account keys.") | |
| continue | |
| except (IOError, json.JSONDecodeError) as e: | |
| logger.warning(f"[single-user] Error reading or parsing file {filepath}: {e}") | |
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's actually correct, i pondered about it for a moment and decided that it's a tradeoff between this and reading the file from disk twice, and decided that for single user configuration it will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @intval - hm, when I create a service account key it does stub out an email with iam.gserviceaccount.com in it, but the actual generated keyfile name is open-webui-444821-b145f5cb467e.json which would not match this check. A user would have to manually rename the key for this logic to work. Thoughts on splitting the service account PR out from the copy & template bits so we can get that merged and figure out the best approach for service accounts separately?
Copilot
AI
Jun 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relying solely on the filename to determine if a file contains service account credentials may be brittle; consider inspecting the JSON content for keys unique to service accounts to improve reliability.
| if "iam.gserviceaccount.com" in filename: | |
| logger.info(f"[single-user] Found service account file: {filepath}") | |
| try: | |
| credentials = service_account.Credentials.from_service_account_file(filepath,scopes=SCOPES) | |
| logger.info(f"[single-user] Successfully loaded service account credentials from {filepath}") | |
| # Force refresh to get a token, since by default it's not set and the library considers then credentials to be invalid | |
| credentials.refresh(Request()) | |
| return credentials | |
| except Exception as e: | |
| logger.warning(f"[single-user] Error loading service account credentials from {filepath}: {e}") | |
| continue | |
| try: | |
| with open(filepath, 'r') as f: | |
| creds_data = json.load(f) | |
| # Check if this is a service account file based on JSON content | |
| if creds_data.get('type') == 'service_account' and 'client_email' in creds_data and 'private_key' in creds_data: | |
| logger.info(f"[single-user] Found service account file: {filepath}") | |
| try: | |
| credentials = service_account.Credentials.from_service_account_info(creds_data, scopes=SCOPES) | |
| logger.info(f"[single-user] Successfully loaded service account credentials from {filepath}") | |
| # Force refresh to get a token, since by default it's not set and the library considers the credentials to be invalid | |
| credentials.refresh(Request()) | |
| return credentials | |
| except Exception as e: | |
| logger.warning(f"[single-user] Error loading service account credentials from {filepath}: {e}") | |
| continue | |
| except (IOError, json.JSONDecodeError) as e: | |
| logger.warning(f"[single-user] Error reading or parsing JSON from {filepath}: {e}") | |
| continue |
Uh oh!
There was an error while loading. Please reload this page.