From 3f754d14e9e194f9596fa32d33a8f09132cfb94b Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Fri, 10 Jul 2026 23:17:58 +0200 Subject: [PATCH] feat(google-drive): new Google Drive integration Drive API v3 (25 commands): shared drive management, change tracking, Drive activity queries, file search/get/create/upload/download/copy/ move/delete, permission list/create/update/delete, and Drive labels. Service-account auth with domain-wide delegation (JWT RS256), runs on a remote engine (requires PyJWT + cryptography). Co-Authored-By: Claude Fable 5 --- integrations/google-drive/manifest.yaml | 316 ++++++++++++++++++ .../google-drive/scripts/activity_list.py | 84 +++++ .../google-drive/scripts/changes_list.py | 97 ++++++ .../scripts/changes_start_token.py | 80 +++++ .../google-drive/scripts/drive_create.py | 86 +++++ .../google-drive/scripts/drive_delete.py | 87 +++++ .../google-drive/scripts/drive_get.py | 82 +++++ .../google-drive/scripts/drives_list.py | 83 +++++ .../google-drive/scripts/file_copy.py | 90 +++++ .../google-drive/scripts/file_create.py | 88 +++++ .../google-drive/scripts/file_delete.py | 98 ++++++ .../google-drive/scripts/file_download.py | 102 ++++++ integrations/google-drive/scripts/file_get.py | 83 +++++ .../google-drive/scripts/file_get_parents.py | 86 +++++ .../google-drive/scripts/file_move.py | 98 ++++++ .../google-drive/scripts/file_replace.py | 96 ++++++ .../google-drive/scripts/file_upload.py | 114 +++++++ .../google-drive/scripts/files_list.py | 91 +++++ .../google-drive/scripts/get_file_labels.py | 85 +++++ .../google-drive/scripts/get_labels.py | 82 +++++ .../google-drive/scripts/modify_label.py | 98 ++++++ .../google-drive/scripts/permission_create.py | 106 ++++++ .../google-drive/scripts/permission_delete.py | 90 +++++ .../google-drive/scripts/permission_update.py | 99 ++++++ .../google-drive/scripts/permissions_list.py | 95 ++++++ .../google-drive/scripts/test_connection.py | 76 +++++ 26 files changed, 2592 insertions(+) create mode 100644 integrations/google-drive/manifest.yaml create mode 100644 integrations/google-drive/scripts/activity_list.py create mode 100644 integrations/google-drive/scripts/changes_list.py create mode 100644 integrations/google-drive/scripts/changes_start_token.py create mode 100644 integrations/google-drive/scripts/drive_create.py create mode 100644 integrations/google-drive/scripts/drive_delete.py create mode 100644 integrations/google-drive/scripts/drive_get.py create mode 100644 integrations/google-drive/scripts/drives_list.py create mode 100644 integrations/google-drive/scripts/file_copy.py create mode 100644 integrations/google-drive/scripts/file_create.py create mode 100644 integrations/google-drive/scripts/file_delete.py create mode 100644 integrations/google-drive/scripts/file_download.py create mode 100644 integrations/google-drive/scripts/file_get.py create mode 100644 integrations/google-drive/scripts/file_get_parents.py create mode 100644 integrations/google-drive/scripts/file_move.py create mode 100644 integrations/google-drive/scripts/file_replace.py create mode 100644 integrations/google-drive/scripts/file_upload.py create mode 100644 integrations/google-drive/scripts/files_list.py create mode 100644 integrations/google-drive/scripts/get_file_labels.py create mode 100644 integrations/google-drive/scripts/get_labels.py create mode 100644 integrations/google-drive/scripts/modify_label.py create mode 100644 integrations/google-drive/scripts/permission_create.py create mode 100644 integrations/google-drive/scripts/permission_delete.py create mode 100644 integrations/google-drive/scripts/permission_update.py create mode 100644 integrations/google-drive/scripts/permissions_list.py create mode 100644 integrations/google-drive/scripts/test_connection.py diff --git a/integrations/google-drive/manifest.yaml b/integrations/google-drive/manifest.yaml new file mode 100644 index 0000000..989ef3d --- /dev/null +++ b/integrations/google-drive/manifest.yaml @@ -0,0 +1,316 @@ +id: google_drive +name: Google Drive +version: 1.0.0 +description: "Google Drive (Drive API v3) — manage shared drives (create/list/get/delete), track file changes (start page token + change lists), query Drive activity, search and read files, create folders and metadata, upload/replace/download file content, copy, move and delete files, manage permissions (list/create/update/delete) and work with Drive labels. Authenticates with a Google service account (JWT bearer flow with domain-wide delegation to impersonate a Workspace user). Runs on a remote engine. Requires the Python 'PyJWT' and 'cryptography' libraries on the engine host (pip install pyjwt cryptography)." +changelog: "1.0.0 — Initial release: shared drive management, changes tracking, Drive activity queries, file search/get/create, content upload/replace/download, copy/move/delete, parents, permissions management, Drive labels, test connection." +category: productivity + +# Per-instance configuration. Authentication uses the Google service-account +# JWT bearer flow: the scripts build a signed RS256 assertion from the service +# account's private_key/client_email, exchange it at the token endpoint for an +# access token, and set the JWT "sub" claim to the impersonated user (domain-wide +# delegation must be granted to the service account's client ID in the Google +# Workspace admin console, with the Drive scopes). user_id is the default user +# to impersonate; every command also accepts a per-call user_id override. +config_schema: + properties: + service_account_json: + type: string + description: "Full service account key JSON (as downloaded from Google Cloud IAM — must contain client_email and private_key)" + x-soar-sensitive: true + user_id: + type: string + description: "Default user email to impersonate via domain-wide delegation (e.g. admin@company.com)" + required: + - service_account_json + - user_id + +commands: + - id: drive_create + name: google-drive-drive-create + description: "Create a new shared drive." + inputs_schema: + properties: + name: { type: string, description: "Name of the shared drive to create" } + hidden: { type: boolean, description: "Create the shared drive hidden from the default view" } + user_id: { type: string, description: "Override the impersonated user" } + required: [name] + outputs_schema: { properties: {} } + - id: drives_list + name: google-drive-drives-list + description: "List the user's shared drives, optionally filtered by a query." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Search query for shared drives (e.g. name contains 'IR')" } + page_size: { type: number, description: "Maximum shared drives per page (default 100)" } + page_token: { type: string, description: "Page token from a previous list call" } + use_domain_admin_access: { type: boolean, description: "Issue the request as a domain administrator (returns all shared drives of the domain)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [] + outputs_schema: { properties: {} } + - id: drive_get + name: google-drive-drive-get + description: "Get a shared drive's metadata by ID." + risk: read + inputs_schema: + properties: + drive_id: { type: string, description: "ID of the shared drive" } + use_domain_admin_access: { type: boolean, description: "Issue the request as a domain administrator" } + user_id: { type: string, description: "Override the impersonated user" } + required: [drive_id] + outputs_schema: { properties: {} } + - id: drive_delete + name: google-drive-drive-delete + description: "Permanently delete a shared drive." + inputs_schema: + properties: + drive_id: { type: string, description: "ID of the shared drive to delete" } + use_domain_admin_access: { type: boolean, description: "Issue the request as a domain administrator" } + allow_item_deletion: { type: boolean, description: "Also delete items inside the shared drive (requires use_domain_admin_access)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [drive_id] + outputs_schema: { properties: {} } + - id: changes_start_token + name: google-drive-changes-start-token + description: "Get the starting page token for listing future changes (per user or per shared drive)." + risk: read + inputs_schema: + properties: + drive_id: { type: string, description: "Shared drive ID to get the start token for (omit for the user's own changes)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [] + outputs_schema: { properties: {} } + - id: changes_list + name: google-drive-changes-list + description: "List the changes for a user or shared drive from a given page token." + risk: read + inputs_schema: + properties: + page_token: { type: string, description: "Token from a previous changes list or from changes-start-token" } + drive_id: { type: string, description: "Shared drive ID to list changes from (omit for the user's own changes)" } + page_size: { type: number, description: "Maximum changes per page (default 100)" } + include_removed: { type: boolean, description: "Include changes for removed/trashed files (default true)" } + include_items_from_all_drives: { type: boolean, description: "Include changes from both My Drive and shared drive items" } + restrict_to_my_drive: { type: boolean, description: "Restrict results to files inside My Drive" } + spaces: { type: string, description: "Comma-separated spaces to query: drive and/or appDataFolder" } + fields: { type: string, description: "Response detail: basic (default fields) or advance (all fields)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [page_token] + outputs_schema: { properties: {} } + - id: activity_list + name: google-drive-activity-list + description: "Query Drive activity (Drive Activity API v2) for an item or a folder subtree." + risk: read + inputs_schema: + properties: + item_name: { type: string, description: "Activity for a single item, format items/ITEM_ID" } + folder_name: { type: string, description: "Activity for everything under a folder, format items/FOLDER_ID (sent as ancestorName)" } + filter: { type: string, description: "Activity filter (e.g. time > \"2026-01-01T00:00:00Z\" detail.action_detail_case:RENAME)" } + page_token: { type: string, description: "Page token from a previous activity query" } + page_size: { type: number, description: "Maximum activities per page" } + user_id: { type: string, description: "Override the impersonated user" } + required: [] + outputs_schema: { properties: {} } + - id: files_list + name: google-drive-files-list + description: "Search and list files, optionally scoped to a shared drive or corpora." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Search query (e.g. name contains 'report' and trashed = false)" } + page_size: { type: number, description: "Maximum files per page (default 100)" } + page_token: { type: string, description: "Page token from a previous list call" } + drive_id: { type: string, description: "Shared drive ID to search in (forces corpora=drive)" } + corpora: { type: string, description: "Bodies of items to query: user, domain, drive or allDrives (default user)" } + include_items_from_all_drives: { type: boolean, description: "Include items from both My Drive and shared drives" } + user_id: { type: string, description: "Override the impersonated user" } + required: [] + outputs_schema: { properties: {} } + - id: file_get + name: google-drive-file-get + description: "Get a file's metadata by ID." + risk: read + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file" } + fields: { type: string, description: "Fields to return (default * — all fields)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: file_create + name: google-drive-file-create + description: "Create a folder or an empty file (metadata only — use file-upload for content)." + inputs_schema: + properties: + file_name: { type: string, description: "Name of the file or folder to create" } + mime_type: { type: string, description: "MIME type (default application/vnd.google-apps.folder — a folder)" } + parent: { type: string, description: "ID of the parent folder or shared drive" } + description: { type: string, description: "Short description of the file" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_name] + outputs_schema: { properties: {} } + - id: file_upload + name: google-drive-file-upload + description: "Upload a new file with content (base64-encoded)." + inputs_schema: + properties: + file_name: { type: string, description: "Name of the file to create" } + content_base64: { type: string, description: "File content, base64-encoded" } + parent: { type: string, description: "ID of the parent folder or shared drive" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_name, content_base64] + outputs_schema: { properties: {} } + - id: file_replace + name: google-drive-file-replace + description: "Replace an existing file's content (base64-encoded)." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file to update" } + content_base64: { type: string, description: "New file content, base64-encoded" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id, content_base64] + outputs_schema: { properties: {} } + - id: file_download + name: google-drive-file-download + description: "Download a file's content — returns file_name, mime_type, size and content_base64." + risk: read + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file to download" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: file_copy + name: google-drive-file-copy + description: "Copy a file, optionally with a new title." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file to copy" } + copy_title: { type: string, description: "Name of the copy (defaults to the original name)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: file_move + name: google-drive-file-move + description: "Move a file between folders (add one parent, remove another)." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file to move" } + add_parent_id: { type: string, description: "ID of the destination folder to add" } + remove_parent_id: { type: string, description: "ID of the current folder to remove" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id, add_parent_id, remove_parent_id] + outputs_schema: { properties: {} } + - id: file_delete + name: google-drive-file-delete + description: "Delete a file — permanently, or move it to the trash with soft_delete." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file to delete" } + soft_delete: { type: boolean, description: "Move the file to the trash instead of deleting it permanently" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: file_get_parents + name: google-drive-file-get-parents + description: "Get the parent folder IDs of a file." + risk: read + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: permissions_list + name: google-drive-permissions-list + description: "List the permissions of a file or shared drive." + risk: read + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file or shared drive" } + page_size: { type: number, description: "Maximum permissions per page" } + page_token: { type: string, description: "Page token from a previous list call" } + use_domain_admin_access: { type: boolean, description: "Issue the request as a domain administrator" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: permission_create + name: google-drive-permission-create + description: "Grant a permission on a file or shared drive (share with a user, group, domain or anyone)." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file or shared drive" } + role: { type: string, description: "Role to grant: reader, commenter, writer, fileOrganizer, organizer or owner (default reader)" } + type: { type: string, description: "Grantee type: user, group, domain or anyone (default user)" } + email_address: { type: string, description: "Email address of the user or group (type user/group)" } + domain: { type: string, description: "Domain name (type domain)" } + send_notification_email: { type: boolean, description: "Send a notification email to the grantee" } + transfer_ownership: { type: boolean, description: "Transfer ownership to the grantee (role owner)" } + move_to_new_owners_root: { type: boolean, description: "Move the file to the new owner's My Drive root on ownership transfer" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: permission_update + name: google-drive-permission-update + description: "Update a permission's role or expiration time." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file or shared drive" } + permission_id: { type: string, description: "ID of the permission to update" } + role: { type: string, description: "New role: reader, commenter, writer, fileOrganizer, organizer or owner" } + expiration_time: { type: string, description: "Expiration time (RFC 3339, e.g. 2026-12-31T23:59:59Z)" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id, permission_id] + outputs_schema: { properties: {} } + - id: permission_delete + name: google-drive-permission-delete + description: "Revoke a permission from a file or shared drive." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file or shared drive" } + permission_id: { type: string, description: "ID of the permission to delete" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id, permission_id] + outputs_schema: { properties: {} } + - id: get_labels + name: google-drive-get-labels + description: "List the Drive label definitions available to the user." + risk: read + inputs_schema: + properties: + user_id: { type: string, description: "Override the impersonated user" } + required: [] + outputs_schema: { properties: {} } + - id: get_file_labels + name: google-drive-get-file-labels + description: "List the labels applied to a file." + risk: read + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id] + outputs_schema: { properties: {} } + - id: modify_label + name: google-drive-modify-label + description: "Apply, update or remove a Drive label on a file." + inputs_schema: + properties: + file_id: { type: string, description: "ID of the file" } + label_id: { type: string, description: "ID of the label to modify" } + field_id: { type: string, description: "ID of the label field to set (selection fields)" } + selection_label_id: { type: string, description: "Selection choice ID to set on the field" } + remove_label: { type: boolean, description: "Remove the label from the file instead of applying it" } + user_id: { type: string, description: "Override the impersonated user" } + required: [file_id, label_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: google-drive-test-connection + description: "Verify service-account credentials and delegation (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/google-drive/scripts/activity_list.py b/integrations/google-drive/scripts/activity_list.py new file mode 100644 index 0000000..1345df8 --- /dev/null +++ b/integrations/google-drive/scripts/activity_list.py @@ -0,0 +1,84 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive.activity.readonly"] +ACTIVITY_URL = "https://driveactivity.googleapis.com/v2/activity:query" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + body = {} + for key, field in (("item_name", "itemName"), + ("folder_name", "ancestorName"), + ("filter", "filter"), + ("page_token", "pageToken"), + ("page_size", "pageSize")): + val = inputs.get(key) + if val not in (None, ""): + body[field] = val + res = request("POST", ACTIVITY_URL, SCOPES, body=body, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/changes_list.py b/integrations/google-drive/scripts/changes_list.py new file mode 100644 index 0000000..739654e --- /dev/null +++ b/integrations/google-drive/scripts/changes_list.py @@ -0,0 +1,97 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + page_token = inputs.get("page_token") + if not page_token: + raise Exception("page_token is required") + params = { + "pageToken": page_token, + "driveId": inputs.get("drive_id"), + "pageSize": inputs.get("page_size") or 100, + "supportsAllDrives": "true", + "spaces": inputs.get("spaces"), + } + include_removed = inputs.get("include_removed") + if include_removed in (None, ""): + params["includeRemoved"] = "true" + else: + params["includeRemoved"] = "true" if str(include_removed).lower() in ("1", "true", "yes") else "false" + for key, param in (("include_items_from_all_drives", "includeItemsFromAllDrives"), + ("restrict_to_my_drive", "restrictToMyDrive")): + val = inputs.get(key) + if val not in (None, ""): + params[param] = "true" if str(val).lower() in ("1", "true", "yes") else "false" + if inputs.get("fields") == "advance": + params["fields"] = "*" + res = request("GET", BASE + "/changes", SCOPES, params=params, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/changes_start_token.py b/integrations/google-drive/scripts/changes_start_token.py new file mode 100644 index 0000000..e4a1741 --- /dev/null +++ b/integrations/google-drive/scripts/changes_start_token.py @@ -0,0 +1,80 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + params = { + "driveId": inputs.get("drive_id"), + "supportsAllDrives": "true", + } + res = request("GET", BASE + "/changes/startPageToken", SCOPES, + params=params, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/drive_create.py b/integrations/google-drive/scripts/drive_create.py new file mode 100644 index 0000000..34c92bf --- /dev/null +++ b/integrations/google-drive/scripts/drive_create.py @@ -0,0 +1,86 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import uuid + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + name = inputs.get("name") + if not name: + raise Exception("name is required") + body = {"name": name} + hidden = inputs.get("hidden") + if hidden not in (None, ""): + body["hidden"] = str(hidden).lower() in ("1", "true", "yes") + res = request("POST", BASE + "/drives", SCOPES, + params={"requestId": str(uuid.uuid4())}, + body=body, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/drive_delete.py b/integrations/google-drive/scripts/drive_delete.py new file mode 100644 index 0000000..72f69ec --- /dev/null +++ b/integrations/google-drive/scripts/drive_delete.py @@ -0,0 +1,87 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + drive_id = inputs.get("drive_id") + if not drive_id: + raise Exception("drive_id is required") + params = {} + for key, param in (("use_domain_admin_access", "useDomainAdminAccess"), + ("allow_item_deletion", "allowItemDeletion")): + val = inputs.get(key) + if val not in (None, ""): + params[param] = "true" if str(val).lower() in ("1", "true", "yes") else "false" + res = request("DELETE", BASE + "/drives/" + urllib.parse.quote(str(drive_id), safe=""), SCOPES, + params=params, subject=inputs.get("user_id")) + if not res: + res = {"ok": True, "drive_id": drive_id} + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/drive_get.py b/integrations/google-drive/scripts/drive_get.py new file mode 100644 index 0000000..90152aa --- /dev/null +++ b/integrations/google-drive/scripts/drive_get.py @@ -0,0 +1,82 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + drive_id = inputs.get("drive_id") + if not drive_id: + raise Exception("drive_id is required") + params = {"fields": "*"} + if str(inputs.get("use_domain_admin_access", "")).lower() in ("1", "true", "yes"): + params["useDomainAdminAccess"] = "true" + res = request("GET", BASE + "/drives/" + urllib.parse.quote(str(drive_id), safe=""), SCOPES, + params=params, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/drives_list.py b/integrations/google-drive/scripts/drives_list.py new file mode 100644 index 0000000..d9feafb --- /dev/null +++ b/integrations/google-drive/scripts/drives_list.py @@ -0,0 +1,83 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + params = { + "q": inputs.get("query"), + "pageSize": inputs.get("page_size") or 100, + "pageToken": inputs.get("page_token"), + "fields": "*", + } + if str(inputs.get("use_domain_admin_access", "")).lower() in ("1", "true", "yes"): + params["useDomainAdminAccess"] = "true" + res = request("GET", BASE + "/drives", SCOPES, params=params, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_copy.py b/integrations/google-drive/scripts/file_copy.py new file mode 100644 index 0000000..7010639 --- /dev/null +++ b/integrations/google-drive/scripts/file_copy.py @@ -0,0 +1,90 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + copy_title = inputs.get("copy_title") + body = {"name": copy_title} if copy_title else {} + + result = request( + "POST", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + "/copy", + SCOPES, + params={"supportsAllDrives": "true", "fields": "*"}, + body=body, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_create.py b/integrations/google-drive/scripts/file_create.py new file mode 100644 index 0000000..49ea7e4 --- /dev/null +++ b/integrations/google-drive/scripts/file_create.py @@ -0,0 +1,88 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_name = inputs.get("file_name") + if not file_name: + raise Exception("file_name is required") + body = { + "name": file_name, + "mimeType": inputs.get("mime_type") or "application/vnd.google-apps.folder", + } + if inputs.get("parent"): + body["parents"] = [inputs["parent"]] + if inputs.get("description"): + body["description"] = inputs["description"] + params = {"supportsAllDrives": "true", "fields": "*"} + res = request("POST", BASE + "/files", SCOPES, params=params, body=body, + subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_delete.py b/integrations/google-drive/scripts/file_delete.py new file mode 100644 index 0000000..b4d2810 --- /dev/null +++ b/integrations/google-drive/scripts/file_delete.py @@ -0,0 +1,98 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + if inputs.get("soft_delete"): + result = request( + "PATCH", + BASE + "/files/" + urllib.parse.quote(file_id, safe=""), + SCOPES, + params={"supportsAllDrives": "true", "fields": "id,name,trashed,trashedTime"}, + body={"trashed": True}, + subject=subject, + ) + print(json.dumps(result)) + return + + request( + "DELETE", + BASE + "/files/" + urllib.parse.quote(file_id, safe=""), + SCOPES, + params={"supportsAllDrives": "true"}, + subject=subject, + ) + print(json.dumps({"ok": True, "file_id": file_id})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_download.py b/integrations/google-drive/scripts/file_download.py new file mode 100644 index 0000000..d81699e --- /dev/null +++ b/integrations/google-drive/scripts/file_download.py @@ -0,0 +1,102 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +import base64 + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + meta = request( + "GET", + BASE + "/files/" + urllib.parse.quote(file_id, safe=""), + SCOPES, + params={"fields": "id,name,mimeType,size", "supportsAllDrives": "true"}, + subject=subject, + ) + + url = (BASE + "/files/" + urllib.parse.quote(file_id, safe="") + + "?alt=media&supportsAllDrives=true") + headers = {"Authorization": "Bearer " + _token(SCOPES, subject)} + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=300) as r: + content = r.read() + + print(json.dumps({ + "id": meta.get("id"), + "file_name": meta.get("name"), + "mime_type": meta.get("mimeType"), + "size": len(content), + "content_base64": base64.b64encode(content).decode("ascii"), + })) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_get.py b/integrations/google-drive/scripts/file_get.py new file mode 100644 index 0000000..d364b26 --- /dev/null +++ b/integrations/google-drive/scripts/file_get.py @@ -0,0 +1,83 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + params = { + "fields": inputs.get("fields") or "*", + "supportsAllDrives": "true", + } + res = request("GET", BASE + "/files/" + urllib.parse.quote(str(file_id), safe=""), SCOPES, + params=params, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_get_parents.py b/integrations/google-drive/scripts/file_get_parents.py new file mode 100644 index 0000000..f067baa --- /dev/null +++ b/integrations/google-drive/scripts/file_get_parents.py @@ -0,0 +1,86 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + result = request( + "GET", + BASE + "/files/" + urllib.parse.quote(file_id, safe=""), + SCOPES, + params={"fields": "id,parents", "supportsAllDrives": "true"}, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_move.py b/integrations/google-drive/scripts/file_move.py new file mode 100644 index 0000000..04120b5 --- /dev/null +++ b/integrations/google-drive/scripts/file_move.py @@ -0,0 +1,98 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + add_parent_id = inputs.get("add_parent_id") + remove_parent_id = inputs.get("remove_parent_id") + if not file_id: + raise Exception("file_id is required") + if not add_parent_id: + raise Exception("add_parent_id is required") + if not remove_parent_id: + raise Exception("remove_parent_id is required") + subject = inputs.get("user_id") + + result = request( + "PATCH", + BASE + "/files/" + urllib.parse.quote(file_id, safe=""), + SCOPES, + params={ + "addParents": add_parent_id, + "removeParents": remove_parent_id, + "supportsAllDrives": "true", + "fields": "*", + }, + body={}, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_replace.py b/integrations/google-drive/scripts/file_replace.py new file mode 100644 index 0000000..c2bd035 --- /dev/null +++ b/integrations/google-drive/scripts/file_replace.py @@ -0,0 +1,96 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +import base64, mimetypes + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + content_base64 = inputs.get("content_base64") + if not file_id: + raise Exception("file_id is required") + if not content_base64: + raise Exception("content_base64 is required") + subject = inputs.get("user_id") + + content = base64.b64decode(content_base64) + content_type = mimetypes.guess_type(inputs.get("file_name") or "")[0] or "application/octet-stream" + + url = ("https://www.googleapis.com/upload/drive/v3/files/" + + urllib.parse.quote(file_id, safe="") + + "?uploadType=media&supportsAllDrives=true&fields=*") + headers = { + "Accept": "application/json", + "Authorization": "Bearer " + _token(SCOPES, subject), + "Content-Type": content_type, + } + req = urllib.request.Request(url, data=content, headers=headers, method="PATCH") + with urllib.request.urlopen(req, timeout=120) as r: + print(json.dumps(json.loads(r.read()))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/file_upload.py b/integrations/google-drive/scripts/file_upload.py new file mode 100644 index 0000000..1c27a7b --- /dev/null +++ b/integrations/google-drive/scripts/file_upload.py @@ -0,0 +1,114 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +import base64, mimetypes + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_name = inputs.get("file_name") + content_base64 = inputs.get("content_base64") + if not file_name: + raise Exception("file_name is required") + if not content_base64: + raise Exception("content_base64 is required") + subject = inputs.get("user_id") + + content = base64.b64decode(content_base64) + content_type = mimetypes.guess_type(file_name)[0] or "application/octet-stream" + + # Step 1: upload raw content + upload_url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=media&supportsAllDrives=true" + headers = { + "Accept": "application/json", + "Authorization": "Bearer " + _token(SCOPES, subject), + "Content-Type": content_type, + } + req = urllib.request.Request(upload_url, data=content, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=120) as r: + created = json.loads(r.read()) + file_id = created.get("id") + if not file_id: + raise Exception("Upload did not return a file id: " + json.dumps(created)) + + # Step 2: set the name (and parent, if any) + result = request( + "PATCH", + BASE + "/files/" + urllib.parse.quote(file_id, safe=""), + SCOPES, + params={ + "addParents": inputs.get("parent") or None, + "supportsAllDrives": "true", + "fields": "*", + }, + body={"name": file_name}, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/files_list.py b/integrations/google-drive/scripts/files_list.py new file mode 100644 index 0000000..6166153 --- /dev/null +++ b/integrations/google-drive/scripts/files_list.py @@ -0,0 +1,91 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + drive_id = inputs.get("drive_id") + corpora = inputs.get("corpora") or "user" + if drive_id: + corpora = "drive" + params = { + "q": inputs.get("query"), + "pageSize": inputs.get("page_size") or 100, + "pageToken": inputs.get("page_token"), + "driveId": drive_id, + "corpora": corpora, + "supportsAllDrives": "true", + "fields": "*", + } + val = inputs.get("include_items_from_all_drives") + if val not in (None, ""): + params["includeItemsFromAllDrives"] = "true" if str(val).lower() in ("1", "true", "yes") else "false" + res = request("GET", BASE + "/files", SCOPES, params=params, subject=inputs.get("user_id")) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/get_file_labels.py b/integrations/google-drive/scripts/get_file_labels.py new file mode 100644 index 0000000..c31d01a --- /dev/null +++ b/integrations/google-drive/scripts/get_file_labels.py @@ -0,0 +1,85 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.labels"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + result = request( + "GET", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + "/listLabels", + SCOPES, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/get_labels.py b/integrations/google-drive/scripts/get_labels.py new file mode 100644 index 0000000..d8d89e5 --- /dev/null +++ b/integrations/google-drive/scripts/get_labels.py @@ -0,0 +1,82 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.labels"] + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + subject = inputs.get("user_id") + + result = request( + "GET", + "https://drivelabels.googleapis.com/v2/labels", + SCOPES, + params={"view": "LABEL_VIEW_FULL"}, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/modify_label.py b/integrations/google-drive/scripts/modify_label.py new file mode 100644 index 0000000..29496b5 --- /dev/null +++ b/integrations/google-drive/scripts/modify_label.py @@ -0,0 +1,98 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.labels"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + label_id = inputs.get("label_id") + if not file_id: + raise Exception("file_id is required") + if not label_id: + raise Exception("label_id is required") + subject = inputs.get("user_id") + + mod = {"kind": "drive#labelModification", "labelId": label_id, "removeLabel": bool(inputs.get("remove_label"))} + if inputs.get("field_id"): + mod["fieldModifications"] = [{ + "kind": "drive#labelFieldModification", + "fieldId": inputs["field_id"], + "setSelectionValues": [inputs.get("selection_label_id")] if inputs.get("selection_label_id") else [], + }] + body = {"kind": "drive#modifyLabelsRequest", "labelModifications": [mod]} + + result = request( + "POST", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + "/modifyLabels", + SCOPES, + body=body, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/permission_create.py b/integrations/google-drive/scripts/permission_create.py new file mode 100644 index 0000000..1aeb1a7 --- /dev/null +++ b/integrations/google-drive/scripts/permission_create.py @@ -0,0 +1,106 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + body = {} + for key, value in ( + ("role", inputs.get("role") or "reader"), + ("type", inputs.get("type") or "user"), + ("emailAddress", inputs.get("email_address")), + ("domain", inputs.get("domain")), + ): + if value: + body[key] = value + + params = {"supportsAllDrives": "true", "fields": "*"} + for src, dst in ( + ("send_notification_email", "sendNotificationEmail"), + ("transfer_ownership", "transferOwnership"), + ("move_to_new_owners_root", "moveToNewOwnersRoot"), + ): + if src in inputs and inputs[src] is not None: + params[dst] = "true" if inputs[src] else "false" + + result = request( + "POST", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + "/permissions", + SCOPES, + params=params, + body=body, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/permission_delete.py b/integrations/google-drive/scripts/permission_delete.py new file mode 100644 index 0000000..1969339 --- /dev/null +++ b/integrations/google-drive/scripts/permission_delete.py @@ -0,0 +1,90 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + permission_id = inputs.get("permission_id") + if not file_id: + raise Exception("file_id is required") + if not permission_id: + raise Exception("permission_id is required") + subject = inputs.get("user_id") + + request( + "DELETE", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + + "/permissions/" + urllib.parse.quote(permission_id, safe=""), + SCOPES, + params={"supportsAllDrives": "true"}, + subject=subject, + ) + print(json.dumps({"ok": True, "permission_id": permission_id})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/permission_update.py b/integrations/google-drive/scripts/permission_update.py new file mode 100644 index 0000000..884afbb --- /dev/null +++ b/integrations/google-drive/scripts/permission_update.py @@ -0,0 +1,99 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + permission_id = inputs.get("permission_id") + if not file_id: + raise Exception("file_id is required") + if not permission_id: + raise Exception("permission_id is required") + subject = inputs.get("user_id") + + body = {} + if inputs.get("role"): + body["role"] = inputs["role"] + if inputs.get("expiration_time"): + body["expirationTime"] = inputs["expiration_time"] + if not body: + raise Exception("nothing to update") + + result = request( + "PATCH", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + + "/permissions/" + urllib.parse.quote(permission_id, safe=""), + SCOPES, + params={"supportsAllDrives": "true", "fields": "*"}, + body=body, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/permissions_list.py b/integrations/google-drive/scripts/permissions_list.py new file mode 100644 index 0000000..6abf086 --- /dev/null +++ b/integrations/google-drive/scripts/permissions_list.py @@ -0,0 +1,95 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + subject = inputs.get("user_id") + + params = { + "pageSize": inputs.get("page_size") or 100, + "pageToken": inputs.get("page_token"), + "supportsAllDrives": "true", + "fields": "*", + } + if inputs.get("use_domain_admin_access"): + params["useDomainAdminAccess"] = "true" + + result = request( + "GET", + BASE + "/files/" + urllib.parse.quote(file_id, safe="") + "/permissions", + SCOPES, + params=params, + subject=subject, + ) + print(json.dumps(result)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/google-drive/scripts/test_connection.py b/integrations/google-drive/scripts/test_connection.py new file mode 100644 index 0000000..a048571 --- /dev/null +++ b/integrations/google-drive/scripts/test_connection.py @@ -0,0 +1,76 @@ +import json, os, sys, time, urllib.parse, urllib.request, urllib.error + +import jwt + +TOKEN_URL = "https://oauth2.googleapis.com/token" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(scopes, subject=None): + cfg = _cfg() + raw = cfg.get("service_account_json", "") + sa = json.loads(raw) if isinstance(raw, str) else raw + if not sa.get("client_email") or not sa.get("private_key"): + raise Exception("service_account_json must contain client_email and private_key") + now = int(time.time()) + aud = sa.get("token_uri") or TOKEN_URL + payload = { + "iss": sa["client_email"], + "scope": " ".join(scopes), + "aud": aud, + "iat": now, + "exp": now + 3600, + } + sub = subject or cfg.get("user_id") or "" + if sub: + payload["sub"] = sub + assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") + data = urllib.parse.urlencode({ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": assertion, + }).encode("utf-8") + req = urllib.request.Request(aud, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def request(method, url, scopes, params=None, body=None, subject=None): + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +SCOPES = ["https://www.googleapis.com/auth/drive"] +BASE = "https://www.googleapis.com/drive/v3" + + +def main(): + res = request("GET", BASE + "/about", SCOPES, params={"fields": "user"}) + if "user" not in res: + raise Exception("Unexpected response from Drive API: " + json.dumps(res)) + print(json.dumps({"ok": True, "user": res.get("user", {}).get("emailAddress", "")})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1)