diff --git a/integrations/microsoft-graph-files/manifest.yaml b/integrations/microsoft-graph-files/manifest.yaml new file mode 100644 index 0000000..f44274c --- /dev/null +++ b/integrations/microsoft-graph-files/manifest.yaml @@ -0,0 +1,286 @@ +id: microsoft_graph_files +name: Microsoft Graph Files +version: 1.0.0 +description: "Microsoft Graph file management for O365 (OneDrive / SharePoint / Teams) — browse sites, drives and content, create folders, upload/replace/download files, manage site permissions, read SharePoint lists, and edit Excel worksheets. App-only authentication (client credentials)." +changelog: "1.0.0 — Initial release: SharePoint sites/drives/content browsing, folder creation, file delete/upload/replace/download (via URL or base64), site permission management (list/create/update/delete), SharePoint list reading (lists/items/get-item), and Excel worksheet editing (add-row/get-worksheet/update-cell). App-only client-credentials authentication." +category: productivity + +# Per-instance configuration. Uses app-only (client credentials) authentication +# against Microsoft Graph. Register an Azure AD application with the required +# application permissions (e.g. Sites.ReadWrite.All, Files.ReadWrite.All) and admin +# consent, then provide the tenant id, client id and client secret. +config_schema: + properties: + host: + type: string + description: "Microsoft Graph base URL" + default: https://graph.microsoft.com + login_url: + type: string + description: "Azure AD login endpoint (change for national clouds, e.g. https://login.microsoftonline.us)" + default: https://login.microsoftonline.com + tenant_id: + type: string + description: "Azure AD tenant ID" + client_id: + type: string + description: "Application (client) ID" + client_secret: + type: string + description: "Client secret" + x-soar-sensitive: true + insecure: + type: boolean + description: "Trust any TLS certificate (not secure)" + default: false + required: + - tenant_id + - client_id + - client_secret + +# Documented for reference; the bundled scripts obtain a bearer token themselves +# via the client-credentials grant and set Authorization: Bearer . +auth: + - id: apikey + type: api_key + in: header + name: Authorization + value_template: "Bearer {{secret}}" + secret_field: client_secret + +commands: + - id: test_connection + name: msgraph-files-auth-test + description: "Verify connectivity and credentials against Microsoft Graph (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + + # ── Sites / drives / content ────────────────────────────────────────────── + - id: list_sharepoint_sites + name: msgraph-list-sharepoint-sites + description: "Search the tenant's SharePoint sites (requires Sites.Read.All)." + risk: read + inputs_schema: + properties: + keyword: { type: string, description: "Search keyword; defaults to all sites (*)" } + required: [] + outputs_schema: { properties: {} } + - id: list_drives_in_site + name: msgraph-list-drives-in-site + description: "List the document libraries (drives) of a site." + risk: read + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (use msgraph-list-sharepoint-sites to find it)" } + limit: { type: number, description: "Maximum results" } + next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" } + required: [] + outputs_schema: { properties: {} } + - id: list_drive_content + name: msgraph-list-drive-content + description: "List files and folders in a drive (object_type: drives/groups/sites/users)." + risk: read + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Folder item ID (default root)" } + limit: { type: number, description: "Maximum results" } + next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" } + required: [object_type, object_type_id] + outputs_schema: { properties: {} } + - id: create_new_folder + name: msgraph-create-new-folder + description: "Create a folder under a parent item in a drive." + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + parent_id: { type: string, description: "Parent item ID (root or a folder ID)" } + folder_name: { type: string, description: "Name of the new folder" } + required: [object_type, object_type_id, parent_id, folder_name] + outputs_schema: { properties: {} } + - id: delete_file + name: msgraph-delete-file + description: "Delete a drive item (file or folder) by its ID." + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Item ID to delete" } + required: [object_type, object_type_id, item_id] + outputs_schema: { properties: {} } + + # ── File transfer (URL / base64) ────────────────────────────────────────── + - id: download_file + name: msgraph-download-file + description: "Return a drive item's metadata and a pre-authenticated download URL. Set as_base64=true to also return the content base64-encoded (small files only)." + risk: read + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Item ID to download" } + as_base64: { type: string, description: "Set true to also include base64 content (small files)" } + required: [object_type, object_type_id, item_id] + outputs_schema: { properties: {} } + - id: upload_new_file + name: msgraph-upload-new-file + description: "Upload a new file into a folder. Provide the content via content_base64 or source_url." + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + parent_id: { type: string, description: "Parent folder ID" } + file_name: { type: string, description: "Name for the uploaded file" } + content_base64: { type: string, description: "File content, base64-encoded" } + source_url: { type: string, description: "URL to fetch the file content from (alternative to content_base64)" } + required: [object_type, object_type_id, parent_id, file_name] + outputs_schema: { properties: {} } + - id: replace_existing_file + name: msgraph-replace-existing-file + description: "Replace the content of an existing file. Provide the new content via content_base64 or source_url." + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Item ID of the file to replace" } + content_base64: { type: string, description: "New file content, base64-encoded" } + source_url: { type: string, description: "URL to fetch the new content from (alternative to content_base64)" } + required: [object_type, object_type_id, item_id] + outputs_schema: { properties: {} } + + # ── Site permissions ────────────────────────────────────────────────────── + - id: list_site_permissions + name: msgraph-list-site-permissions + description: "List application permissions on a site, or a single permission when permission_id is given. Provide site_id or site_name." + risk: read + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (resolved to an ID; or provide site_id)" } + permission_id: { type: string, description: "A specific permission ID to retrieve" } + required: [] + outputs_schema: { properties: {} } + - id: create_site_permissions + name: msgraph-create-site-permissions + description: "Grant an application permission (read/write/owner) on a site." + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (or provide site_id)" } + role: { type: string, description: "read, write or owner" } + app_id: { type: string, description: "Application (client) ID to grant" } + display_name: { type: string, description: "Display name of the application" } + required: [role, app_id, display_name] + outputs_schema: { properties: {} } + - id: update_site_permissions + name: msgraph-update-site-permissions + description: "Update the role of an existing site permission." + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (or provide site_id)" } + permission_id: { type: string, description: "Permission ID to update" } + role: { type: string, description: "read, write or owner" } + required: [permission_id, role] + outputs_schema: { properties: {} } + - id: delete_site_permissions + name: msgraph-delete-site-permissions + description: "Delete an application permission from a site." + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (or provide site_id)" } + permission_id: { type: string, description: "Permission ID to delete" } + required: [permission_id] + outputs_schema: { properties: {} } + + # ── SharePoint lists ────────────────────────────────────────────────────── + - id: list_sharepoint_lists + name: msgraph-list-sharepoint-lists + description: "List the SharePoint lists of a site. Provide site_id or site_name." + risk: read + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (or provide site_id)" } + limit: { type: number, description: "Maximum results" } + next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" } + required: [] + outputs_schema: { properties: {} } + - id: list_sharepoint_list_items + name: msgraph-list-sharepoint-list-items + description: "List items of a SharePoint list (with their fields). Provide list_id or list_name." + risk: read + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (or provide site_id)" } + list_id: { type: string, description: "List ID (or provide list_name)" } + list_name: { type: string, description: "List name (or provide list_id)" } + limit: { type: number, description: "Maximum results" } + filter: { type: string, description: "OData $filter expression" } + orderby: { type: string, description: "OData $orderby expression" } + fields_select: { type: string, description: "Comma-separated list of field columns to expand" } + next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" } + required: [] + outputs_schema: { properties: {} } + - id: get_sharepoint_list_item + name: msgraph-get-sharepoint-list-item + description: "Get a single SharePoint list item with its fields. Provide list_id or list_name." + risk: read + inputs_schema: + properties: + site_id: { type: string, description: "Site ID (or provide site_name)" } + site_name: { type: string, description: "Site name (or provide site_id)" } + list_id: { type: string, description: "List ID (or provide list_name)" } + list_name: { type: string, description: "List name (or provide list_id)" } + item_id: { type: string, description: "List item ID" } + required: [item_id] + outputs_schema: { properties: {} } + + # ── Excel ───────────────────────────────────────────────────────────────── + - id: excel_add_row + name: msgraph-excel-add-row + description: "Append a row to an Excel worksheet. 'data' is a JSON object mapping column number (1-based) to value, e.g. {\"1\":\"foo\",\"2\":42}." + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Workbook (xlsx) item ID" } + worksheet_name: { type: string, description: "Worksheet name" } + data: { type: object, description: "Object mapping 1-based column number to value" } + start_row: { type: number, description: "Row to start appending from (default 1)" } + required: [object_type_id, item_id, worksheet_name, data] + outputs_schema: { properties: {} } + - id: excel_get_worksheet + name: msgraph-excel-get-worksheet + description: "Read a range from an Excel worksheet (formatted text values)." + risk: read + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Workbook (xlsx) item ID" } + worksheet_name: { type: string, description: "Worksheet name" } + range: { type: string, description: "Range address, e.g. A1:Z500 (default A1:Z500)" } + required: [object_type_id, item_id, worksheet_name] + outputs_schema: { properties: {} } + - id: excel_update_cell + name: msgraph-excel-update-cell + description: "Overwrite a single Excel cell. 'column' may be a number or 'Col_2'." + inputs_schema: + properties: + object_type: { type: string, description: "drives, groups, sites or users" } + object_type_id: { type: string, description: "The resource ID" } + item_id: { type: string, description: "Workbook (xlsx) item ID" } + worksheet_name: { type: string, description: "Worksheet name" } + row_index: { type: number, description: "1-based row index" } + column: { type: string, description: "Column number or 'Col_2'" } + value: { type: string, description: "New cell value" } + required: [object_type_id, item_id, worksheet_name, row_index, column, value] + outputs_schema: { properties: {} } diff --git a/integrations/microsoft-graph-files/scripts/create_new_folder.py b/integrations/microsoft-graph-files/scripts/create_new_folder.py new file mode 100644 index 0000000..7ed0a9b --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/create_new_folder.py @@ -0,0 +1,164 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + parent = drive_item_base(I["object_type"], I["object_type_id"], I["parent_id"]) + body = {"name": I["folder_name"], "folder": {}, "@microsoft.graph.conflictBehavior": "rename"} + out(graph("POST", parent + "/children", json_body=body)) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/create_site_permissions.py b/integrations/microsoft-graph-files/scripts/create_site_permissions.py new file mode 100644 index 0000000..23c3bd3 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/create_site_permissions.py @@ -0,0 +1,167 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + body = { + "roles": _list(I["role"]), + "grantedToIdentities": [{"application": {"id": I["app_id"], "displayName": I["display_name"]}}], + } + out(graph("POST", "/sites/" + site_id + "/permissions", json_body=body)) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/delete_file.py b/integrations/microsoft-graph-files/scripts/delete_file.py new file mode 100644 index 0000000..143629f --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/delete_file.py @@ -0,0 +1,164 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + uri = drive_item_base(I["object_type"], I["object_type_id"], I["item_id"]) + graph("DELETE", uri, resp="response") + out({"deleted": I["item_id"]}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/delete_site_permissions.py b/integrations/microsoft-graph-files/scripts/delete_site_permissions.py new file mode 100644 index 0000000..5e87a87 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/delete_site_permissions.py @@ -0,0 +1,165 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + uri = "/sites/" + site_id + "/permissions/" + I["permission_id"] + graph("DELETE", uri, resp="response") + out({"deleted": I["permission_id"], "site_id": site_id}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/download_file.py b/integrations/microsoft-graph-files/scripts/download_file.py new file mode 100644 index 0000000..3c241ab --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/download_file.py @@ -0,0 +1,168 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + base = drive_item_base(I["object_type"], I["object_type_id"], I["item_id"]) + meta = graph("GET", base) + url = meta.get("@microsoft.graph.downloadUrl") + result = {"id": meta.get("id"), "name": meta.get("name"), "size": meta.get("size"), "download_url": url} + if _bool(I.get("as_base64")) and url: + result["content_base64"] = base64.b64encode(fetch_url(url)).decode("ascii") + out(result) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/excel_add_row.py b/integrations/microsoft-graph-files/scripts/excel_add_row.py new file mode 100644 index 0000000..c5d2628 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/excel_add_row.py @@ -0,0 +1,179 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + data = I["data"] + if isinstance(data, str): + data = json.loads(data) + if not data: + fail("data is empty") + return + base = worksheet_base(I["object_type"] or "sites", I["object_type_id"], I["item_id"], I["worksheet_name"]) + used = graph("GET", base + "/usedRange", params={"$select": "rowCount"}) + row_count = int(used.get("rowCount", 0)) + start_row = int(I.get("start_row") or 1) + target_row = max(start_row, row_count + 1) + max_col = max(int(k) for k in data) + row_values = [""] * max_col + for k, v in data.items(): + row_values[int(k) - 1] = v + addr = "A" + str(target_row) + ":" + col_letter(max_col) + str(target_row) + res = graph("PATCH", base + "/range(address='" + addr + "')", json_body={"values": [row_values]}) + out({"address": res.get("address", addr), "row": target_row, "values": row_values}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/excel_get_worksheet.py b/integrations/microsoft-graph-files/scripts/excel_get_worksheet.py new file mode 100644 index 0000000..cfe5279 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/excel_get_worksheet.py @@ -0,0 +1,165 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + base = worksheet_base(I.get("object_type") or "sites", I["object_type_id"], I["item_id"], I["worksheet_name"]) + range_addr = I.get("range") or "A1:Z500" + res = graph("GET", base + "/range(address='" + range_addr + "')") + out({"address": res.get("address"), "values": res.get("text") or res.get("values")}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/excel_update_cell.py b/integrations/microsoft-graph-files/scripts/excel_update_cell.py new file mode 100644 index 0000000..9fe9b60 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/excel_update_cell.py @@ -0,0 +1,170 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + match = re.search(r"\d+", str(I["column"])) + if not match: + fail("Invalid column: " + str(I["column"])) + return + col_num = int(match.group()) + addr = col_letter(col_num) + str(int(I["row_index"])) + base = worksheet_base(I.get("object_type") or "sites", I["object_type_id"], I["item_id"], I["worksheet_name"]) + graph("PATCH", base + "/range(address='" + addr + "')", json_body={"values": [[I["value"]]]}) + out({"cell": addr, "value": I["value"]}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/get_sharepoint_list_item.py b/integrations/microsoft-graph-files/scripts/get_sharepoint_list_item.py new file mode 100644 index 0000000..dafc02c --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/get_sharepoint_list_item.py @@ -0,0 +1,165 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + list_id = resolve_list_id(site_id) + uri = "/sites/" + site_id + "/lists/" + list_id + "/items/" + I["item_id"] + out(graph("GET", uri, params={"$expand": "fields"})) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/list_drive_content.py b/integrations/microsoft-graph-files/scripts/list_drive_content.py new file mode 100644 index 0000000..755b157 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/list_drive_content.py @@ -0,0 +1,168 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + if I.get("next_page_url"): + res = graph("GET", I["next_page_url"], params={"$top": I.get("limit")}) + else: + item = I.get("item_id") or "root" + uri = drive_item_base(I["object_type"], I["object_type_id"], item) + "/children" + res = graph("GET", uri, params={"$top": I.get("limit")}) + out({"children": res.get("value", []), "next": res.get("@odata.nextLink")}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/list_drives_in_site.py b/integrations/microsoft-graph-files/scripts/list_drives_in_site.py new file mode 100644 index 0000000..2568263 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/list_drives_in_site.py @@ -0,0 +1,170 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + params = {"$top": I.get("limit")} + if I.get("next_page_url"): + res = graph("GET", I["next_page_url"], params=params) + elif I.get("site_id"): + res = graph("GET", "/sites/" + I["site_id"] + "/drives", params=params) + else: + fail("Provide site_id or next_page_url") + return + out({"drives": res.get("value", []), "next": res.get("@odata.nextLink")}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/list_sharepoint_list_items.py b/integrations/microsoft-graph-files/scripts/list_sharepoint_list_items.py new file mode 100644 index 0000000..82e76f2 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/list_sharepoint_list_items.py @@ -0,0 +1,170 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + list_id = resolve_list_id(site_id) + expand = "fields($select=" + I["fields_select"] + ")" if I.get("fields_select") else "fields" + params = {"$top": I.get("limit"), "$filter": I.get("filter"), "$orderby": I.get("orderby"), "$expand": expand} + if I.get("next_page_url"): + res = graph("GET", I["next_page_url"], params={"$top": I.get("limit")}) + else: + res = graph("GET", "/sites/" + site_id + "/lists/" + list_id + "/items", params=params) + out({"items": res.get("value", []), "next": res.get("@odata.nextLink")}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/list_sharepoint_lists.py b/integrations/microsoft-graph-files/scripts/list_sharepoint_lists.py new file mode 100644 index 0000000..2d8128d --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/list_sharepoint_lists.py @@ -0,0 +1,167 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + if I.get("next_page_url"): + res = graph("GET", I["next_page_url"], params={"$top": I.get("limit")}) + else: + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": I.get("limit")}) + out({"lists": res.get("value", []), "next": res.get("@odata.nextLink")}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/list_sharepoint_sites.py b/integrations/microsoft-graph-files/scripts/list_sharepoint_sites.py new file mode 100644 index 0000000..bd9b625 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/list_sharepoint_sites.py @@ -0,0 +1,163 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + res = graph("GET", "/sites", params={"search": I.get("keyword") or "*"}) + out({"sites": res.get("value", []), "next": res.get("@odata.nextLink")}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/list_site_permissions.py b/integrations/microsoft-graph-files/scripts/list_site_permissions.py new file mode 100644 index 0000000..88e2733 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/list_site_permissions.py @@ -0,0 +1,166 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + pid = I.get("permission_id") + uri = "/sites/" + site_id + "/permissions" + ("/" + pid if pid else "") + res = graph("GET", uri) + out(res if pid else {"permissions": res.get("value", [])}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/replace_existing_file.py b/integrations/microsoft-graph-files/scripts/replace_existing_file.py new file mode 100644 index 0000000..b62467a --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/replace_existing_file.py @@ -0,0 +1,164 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + content = input_bytes() + uri = drive_item_base(I["object_type"], I["object_type_id"], I["item_id"]) + "/content" + out(graph("PUT", uri, data=content)) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/test_connection.py b/integrations/microsoft-graph-files/scripts/test_connection.py new file mode 100644 index 0000000..4ee507f --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/test_connection.py @@ -0,0 +1,162 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + +def run(): + graph("GET", "/sites", params={"search": "*", "$top": 1}) + out({"ok": True}) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/update_site_permissions.py b/integrations/microsoft-graph-files/scripts/update_site_permissions.py new file mode 100644 index 0000000..fcbf252 --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/update_site_permissions.py @@ -0,0 +1,164 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + site_id = resolve_site_id() + uri = "/sites/" + site_id + "/permissions/" + I["permission_id"] + out(graph("PATCH", uri, json_body={"roles": _list(I["role"])})) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e)) diff --git a/integrations/microsoft-graph-files/scripts/upload_new_file.py b/integrations/microsoft-graph-files/scripts/upload_new_file.py new file mode 100644 index 0000000..f789fcd --- /dev/null +++ b/integrations/microsoft-graph-files/scripts/upload_new_file.py @@ -0,0 +1,166 @@ +import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error + +S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) +I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + +GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/") +LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/") + + +def out(value): + print(json.dumps(value, default=str)) + + +def fail(message, **extra): + payload = {"error": message} + payload.update(extra) + print(json.dumps(payload, default=str)) + sys.exit(1) + + +def _bool(v): + return str(v).lower() in ("1", "true", "yes") + + +def _list(v): + if isinstance(v, list): + return v + return [x.strip() for x in str(v).split(",") if x.strip()] + + +def _ctx(): + return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context() + + +_TOKEN = None + + +def token(): + global _TOKEN + if _TOKEN is not None: + return _TOKEN + data = urllib.parse.urlencode({ + "client_id": S.get("client_id", ""), + "client_secret": S.get("client_secret", ""), + "grant_type": "client_credentials", + "scope": GRAPH + "/.default", + }).encode("utf-8") + url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r: + _TOKEN = json.loads(r.read())["access_token"] + except urllib.error.HTTPError as e: + raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace")) + return _TOKEN + + +def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"): + url = path if path.startswith("http") else (GRAPH + "/v1.0" + path) + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean) + h = {"Authorization": "Bearer " + token(), "Accept": "application/json"} + body = None + if json_body is not None: + body = json.dumps(json_body).encode("utf-8") + h["Content-Type"] = "application/json" + elif data is not None: + body = data + h["Content-Type"] = "application/octet-stream" + if headers: + h.update(headers) + req = urllib.request.Request(url, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + raw = r.read() + if resp == "response": + return {"status": r.status} + if resp == "content": + return raw + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace")) + + +def fetch_url(url): + """Fetch raw bytes from an arbitrary URL (no Graph auth header).""" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r: + return r.read() + + +def input_bytes(): + """Resolve upload content from content_base64 or source_url.""" + if I.get("content_base64"): + return base64.b64decode(I["content_base64"]) + if I.get("source_url"): + return fetch_url(I["source_url"]) + fail("Provide either content_base64 or source_url") + + +def col_letter(n): + s = "" + n = int(n) + while n > 0: + n, rem = divmod(n - 1, 26) + s = chr(65 + rem) + s + return s + + +def resolve_site_id(): + if I.get("site_id"): + return I["site_id"] + name = I.get("site_name") + if not name: + fail("Provide site_id or site_name") + res = graph("GET", "/sites", params={"search": name}) + value = res.get("value", []) + if not value: + fail("Site not found: " + name) + return value[0]["id"] + + +def resolve_list_id(site_id): + if I.get("list_id"): + return I["list_id"] + name = I.get("list_name") + if not name: + fail("Provide list_id or list_name") + res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999}) + for lst in res.get("value", []): + if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower(): + return lst["id"] + fail("List not found: " + name) + + +def drive_item_base(object_type, object_type_id, item_id): + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + + +def worksheet_base(object_type, object_type_id, item_id, worksheet): + ws = urllib.parse.quote(worksheet) + if object_type == "drives": + return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws + return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws + + + +def run(): + content = input_bytes() + name = urllib.parse.quote(I["file_name"]) + base = drive_item_base(I["object_type"], I["object_type_id"], I["parent_id"]) + uri = base + ":/" + name + ":/content" + out(graph("PUT", uri, data=content)) + + +try: + run() +except SystemExit: + raise +except Exception as e: + fail(str(e))