From 81c5d0bdec356e35f0107e8daac5e0257a35fb84 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sat, 31 Aug 2024 00:23:28 -0700 Subject: [PATCH 1/4] add basic google drive api usage --- requirements.txt | 1 + src/google_api/__init__.py | 0 src/google_api/constants.py | 24 +++++++++++ src/google_api/internals.py | 69 ++++++++++++++++++++++++++++++++ tests/integration/test_google.py | 8 ++++ 5 files changed, 102 insertions(+) create mode 100644 src/google_api/__init__.py create mode 100644 src/google_api/constants.py create mode 100644 src/google_api/internals.py create mode 100644 tests/integration/test_google.py diff --git a/requirements.txt b/requirements.txt index 1a2989f..7b350cf 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ uvicorn[standard]==0.27.1 sqlalchemy==2.0.27 asyncpg==0.29.0 alembic==1.13.1 +google-api-python-client==2.143.0 # minor pyOpenSSL==24.0.0 # for generating cryptographically secure random numbers diff --git a/src/google_api/__init__.py b/src/google_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/google_api/constants.py b/src/google_api/constants.py new file mode 100644 index 0000000..948ffc5 --- /dev/null +++ b/src/google_api/constants.py @@ -0,0 +1,24 @@ +import os +import pathlib + +_this_file_path = pathlib.Path(__file__).parent.resolve() + +# any officer from the past 5 semesters has access to these +# TODO: ask the pres if we still want these rules, or not +FIVE_SEM_OFFICER_ACCESS = [ + "CSSS@SFU", +] + +EXECUTIVE_ACCESS = [ + "CSSS Gallery", + "CSSS@SFU", + "Deep-Exec", + "Exec_Photos", + "Private Gallery", +] + +GOOGLE_API_SCOPES = [ + "https://www.googleapis.com/auth/drive" +] + +SERVICE_ACCOUNT_KEY_PATH = str((_this_file_path / "../../google_key.json").resolve()) diff --git a/src/google_api/internals.py b/src/google_api/internals.py new file mode 100644 index 0000000..12ae1c3 --- /dev/null +++ b/src/google_api/internals.py @@ -0,0 +1,69 @@ +# google workspace (shared drives) + google drive api + +import io +import os + +import requests +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload +from requests import Response + +from google_api.constants import GOOGLE_API_SCOPES, SERVICE_ACCOUNT_KEY_PATH + +# TODO: understand how these work +credentials = service_account.Credentials.from_service_account_file( + filename=SERVICE_ACCOUNT_KEY_PATH, + scopes=GOOGLE_API_SCOPES +) +delegated_credentials = credentials.with_subject("csss@sfucsss.org") +service = build("drive", "v3", credentials=delegated_credentials) + +# TODO: view access to root folders / organization +# TODO: give access to root folders / organization + +# TODO: how to even do this correctly? +def _get_access_token() -> str: + pass + +def _google_drive_request( + url: str, + token: str, # TODO: what kind of token is it? +) -> Response: + result = requests.get( + url, + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + ) + return result + +def _list_shared_drives(token: str): + response = _google_drive_request( + "https://www.googleapis.com/drive/v3/drives?", + token + ) + return str(response) + +def _list_shared_drives_2(): + # Call the Drive v3 API + # TODO: see if we can make API calls directly without this funky stuff... ? + results = ( + service + .drives() + .list( + #pageSize = 50, + #q = "name contains 'CSSS'", + #useDomainAdminAccess = True, + ) + .execute() + ) + + # get the results + print(results) + +def is_email_valid(): + # TODO: determine if a google email is valid + pass diff --git a/tests/integration/test_google.py b/tests/integration/test_google.py new file mode 100644 index 0000000..99a3111 --- /dev/null +++ b/tests/integration/test_google.py @@ -0,0 +1,8 @@ +from google_api import internals + + +def test__list_drives(): + try: + internals._list_shared_drives_2() + except Exception as e: + print(f"got {e}") From fe4e08263fe704c3dc698071eb7aff0caddaab79 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:42:02 -0700 Subject: [PATCH 2/4] finish off lightweight google api module --- src/google_api/constants.py | 11 +++- src/google_api/internals.py | 91 +++++++++++++++++--------------- src/utils.py | 4 ++ tests/integration/test_google.py | 25 +++++++-- 4 files changed, 82 insertions(+), 49 deletions(-) diff --git a/src/google_api/constants.py b/src/google_api/constants.py index 948ffc5..61aa5b9 100644 --- a/src/google_api/constants.py +++ b/src/google_api/constants.py @@ -1,7 +1,8 @@ import os import pathlib -_this_file_path = pathlib.Path(__file__).parent.resolve() +# this google account runs the google workspace for executives +GOOGLE_WORKSPACE_ACCOUNT = "csss@sfucsss.org" # any officer from the past 5 semesters has access to these # TODO: ask the pres if we still want these rules, or not @@ -17,8 +18,16 @@ "Private Gallery", ] +# scopes are like permissions to google GOOGLE_API_SCOPES = [ + # google drive permission "https://www.googleapis.com/auth/drive" ] +GOOGLE_DRIVE_PERMISSION_ROLES = [ + "organizer", + "fileOrganizer", +] + +_this_file_path = pathlib.Path(__file__).parent.resolve() SERVICE_ACCOUNT_KEY_PATH = str((_this_file_path / "../../google_key.json").resolve()) diff --git a/src/google_api/internals.py b/src/google_api/internals.py index 12ae1c3..5a9aa65 100644 --- a/src/google_api/internals.py +++ b/src/google_api/internals.py @@ -1,56 +1,20 @@ # google workspace (shared drives) + google drive api -import io -import os - -import requests from google.oauth2 import service_account from googleapiclient.discovery import build -from googleapiclient.errors import HttpError -from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload -from requests import Response -from google_api.constants import GOOGLE_API_SCOPES, SERVICE_ACCOUNT_KEY_PATH +from google_api.constants import GOOGLE_API_SCOPES, GOOGLE_WORKSPACE_ACCOUNT, SERVICE_ACCOUNT_KEY_PATH # TODO: understand how these work credentials = service_account.Credentials.from_service_account_file( filename=SERVICE_ACCOUNT_KEY_PATH, scopes=GOOGLE_API_SCOPES ) -delegated_credentials = credentials.with_subject("csss@sfucsss.org") +delegated_credentials = credentials.with_subject(GOOGLE_WORKSPACE_ACCOUNT) service = build("drive", "v3", credentials=delegated_credentials) -# TODO: view access to root folders / organization -# TODO: give access to root folders / organization - -# TODO: how to even do this correctly? -def _get_access_token() -> str: - pass - -def _google_drive_request( - url: str, - token: str, # TODO: what kind of token is it? -) -> Response: - result = requests.get( - url, - headers={ - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } - ) - return result - -def _list_shared_drives(token: str): - response = _google_drive_request( - "https://www.googleapis.com/drive/v3/drives?", - token - ) - return str(response) - -def _list_shared_drives_2(): - # Call the Drive v3 API - # TODO: see if we can make API calls directly without this funky stuff... ? - results = ( +def _list_shared_drives() -> list: + return ( service .drives() .list( @@ -61,9 +25,48 @@ def _list_shared_drives_2(): .execute() ) - # get the results - print(results) +def list_drive_permissions(drive_id: str) -> list: + return ( + service + .permissions() + .list( + fileId = drive_id, + # important to find the shared drive + supportsAllDrives = True, + fields = "*", + ) + .execute() + ) + +def create_drive_permission(drive_id: str, permission: dict): + return ( + service + .permissions() + .create( + fileId = drive_id, + + # TODO: update message + emailMessage = "You were just given permission to an SFU CSSS shared google drive!", + sendNotificationEmail = True, + supportsAllDrives = True, + + body=permission, + ) + .execute() + ) + +def delete_drive_permission(drive_id: str, permission_id: str): + return ( + service + .permissions() + .delete( + fileId = drive_id, + permissionId = permission_id, + supportsAllDrives = True, + ) + .execute() + ) -def is_email_valid(): - # TODO: determine if a google email is valid +def remove_drive_access(): + # TODO: remove drive permissions pass diff --git a/src/utils.py b/src/utils.py index 5b56970..39758e1 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,4 @@ +import re from datetime import datetime from officers.tables import OfficerInfo, OfficerTerm @@ -32,3 +33,6 @@ def is_valid_phone_number(phone_number: str) -> bool: len(phone_number) == 10 and phone_number.isnumeric() ) + +def is_valid_email(email: str): + return not re.match(r"^[^@]+@[^@]+\.[a-zA-Z]*$", email) diff --git a/tests/integration/test_google.py b/tests/integration/test_google.py index 99a3111..5b41c59 100644 --- a/tests/integration/test_google.py +++ b/tests/integration/test_google.py @@ -2,7 +2,24 @@ def test__list_drives(): - try: - internals._list_shared_drives_2() - except Exception as e: - print(f"got {e}") + """should not fail""" + drive_list = internals._list_shared_drives() + print(drive_list) + + drive_id = drive_list["drives"][0]["id"] + print(drive_id) + + permissions = internals.list_drive_permissions(drive_id) + print(permissions) + + # NOTE: this will raise an exception if the email address is a non-google account + """ + internals.create_drive_permission( + drive_id, + { + "type": "user", + "emailAddress": "tester_123591735013000019@gmail2.ca", + "role": "fileOrganizer", + } + ) + """ From fe8f79770643dc2e7f8c74a831fff865d42265d8 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:46:22 -0700 Subject: [PATCH 3/4] appease linter --- .gitignore | 4 ++++ src/google_api/constants.py | 1 + src/officers/types.py | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e869bd6..12a75c6 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ +# main src/run/ logs/ +# google drive api +google_key.json + # Python - Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/src/google_api/constants.py b/src/google_api/constants.py index 61aa5b9..f547817 100644 --- a/src/google_api/constants.py +++ b/src/google_api/constants.py @@ -24,6 +24,7 @@ "https://www.googleapis.com/auth/drive" ] +# TODO: make this into an enum, or something GOOGLE_DRIVE_PERMISSION_ROLES = [ "organizer", "fileOrganizer", diff --git a/src/officers/types.py b/src/officers/types.py index 7546991..12a2784 100644 --- a/src/officers/types.py +++ b/src/officers/types.py @@ -166,8 +166,8 @@ def serializable_dict(self): @staticmethod def from_data( - term: officers.tables.OfficerTerm, - officer_info: officers.tables.OfficerInfo, + term: OfficerTerm, + officer_info: OfficerInfo, include_private: bool, is_active: bool, ) -> OfficerData: From dfa3b290005a4e687f162d8b871459f460ada206 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:50:13 -0700 Subject: [PATCH 4/4] remove dead code --- src/google_api/internals.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/google_api/internals.py b/src/google_api/internals.py index 5a9aa65..2246076 100644 --- a/src/google_api/internals.py +++ b/src/google_api/internals.py @@ -66,7 +66,3 @@ def delete_drive_permission(drive_id: str, permission_id: str): ) .execute() ) - -def remove_drive_access(): - # TODO: remove drive permissions - pass