From f41d60f1162265b15baf0f1863db87dffdf86ef1 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Fri, 3 Jul 2026 17:58:42 -0400 Subject: [PATCH] Phase 2 (partial): Move constants and calendar ACL helpers --- src/gam/cmd/calendar.py | 50 +++++++++++++-- src/gam/cmd/resources.py | 2 +- src/gam/cmd/userservices.py | 2 +- src/gam/constants.py | 123 ++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 8 deletions(-) diff --git a/src/gam/cmd/calendar.py b/src/gam/cmd/calendar.py index 9b4f6839..d9c934c2 100644 --- a/src/gam/cmd/calendar.py +++ b/src/gam/cmd/calendar.py @@ -93,12 +93,55 @@ from gam.util.errors import ( unknownArgumentExit, ) from gam.util.fileio import UNKNOWN -from gam.util.output import executeBatch, setSysExitRC +from gam.util.output import executeBatch, formatKeyValueList, setSysExitRC from gam.constants import DAYS_OF_WEEK, GOOGLE_MEETID_FORMAT_REQUIRED, GOOGLE_MEETID_PATTERN, NO_ENTITIES_FOUND_RC Act = glaction.GamAction() Ent = glentity.GamEntity() Ind = glindent.GamIndent() + + +# ACL utility functions (moved from gam/__init__.py) +def ACLRuleDict(rule): + if rule['scope']['type'] != 'default': + return {'Scope': f'{rule["scope"]["type"]}:{rule["scope"]["value"]}', 'Role': rule['role']} + return {'Scope': f'{rule["scope"]["type"]}', 'Role': rule['role']} + +def ACLRuleKeyValueList(rule): + if rule['scope']['type'] != 'default': + return ['Scope', f'{rule["scope"]["type"]}:{rule["scope"]["value"]}', 'Role', rule['role']] + return ['Scope', f'{rule["scope"]["type"]}', 'Role', rule['role']} + +def formatACLRule(rule): + return formatKeyValueList('(', ACLRuleKeyValueList(rule), ')') + +def formatACLScopeRole(scope, role): + if role: + return formatKeyValueList('(', ['Scope', scope, 'Role', role], ')') + return formatKeyValueList('(', ['Scope', scope], ')') + +def normalizeRuleId(ruleId): + ruleIdParts = ruleId.split(':', 1) + if (len(ruleIdParts) == 1) or not ruleIdParts[1]: + if ruleIdParts[0] == 'default': + return ruleId + if ruleIdParts[0] == 'domain': + return f'domain:{GC.Values[GC.DOMAIN]}' + return f'user:{normalizeEmailAddressOrUID(ruleIdParts[0], noUid=True)}' + if ruleIdParts[0] in {'user', 'group'}: + return f'{ruleIdParts[0]}:{normalizeEmailAddressOrUID(ruleIdParts[1], noUid=True)}' + return ruleId + +def makeRoleRuleIdBody(role, ruleId): + ruleIdParts = ruleId.split(':', 1) + if len(ruleIdParts) == 1: + if ruleIdParts[0] == 'default': + return {'role': role, 'scope': {'type': ruleIdParts[0]}} + if ruleIdParts[0] == 'domain': + return {'role': role, 'scope': {'type': ruleIdParts[0], 'value': GC.Values[GC.DOMAIN]}} + return {'role': role, 'scope': {'type': 'user', 'value': ruleIdParts[0]}} + return {'role': role, 'scope': {'type': ruleIdParts[0], 'value': ruleIdParts[1]}} + Cmd = glclargs.GamCLArgs() @@ -234,7 +277,6 @@ def _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, j, jcount, ACLScop return (calId, cal, ruleIds, kcount) def _processCalendarACLs(cal, function, entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications): - from gam import formatACLScopeRole, makeRoleRuleIdBody result = True if function == 'insert': kwargs = {'body': makeRoleRuleIdBody(role, ruleId), 'fields': '', 'sendNotifications': sendNotifications} @@ -264,7 +306,6 @@ def _processCalendarACLs(cal, function, entityType, calId, j, jcount, k, kcount, return result def _createCalendarACLs(cal, entityType, calId, j, jcount, role, ruleIds, kcount, sendNotifications): - from gam import normalizeRuleId Ind.Increment() k = 0 for ruleId in ruleIds: @@ -294,7 +335,6 @@ def doCalendarsCreateACLs(calIds): _doCalendarsCreateACLs(None, None, None, calIds, len(calIds), role, ACLScopeEntity, sendNotifications) def _updateDeleteCalendarACLs(cal, function, entityType, calId, j, jcount, role, ruleIds, kcount, sendNotifications): - from gam import normalizeRuleId Ind.Increment() k = 0 for ruleId in ruleIds: @@ -334,7 +374,6 @@ def doCalendarsDeleteACLs(calIds): _doUpdateDeleteCalendarACLs(None, None, None, 'delete', calIds, len(calIds), ACLScopeEntity, role, False) def _showCalendarACL(user, entityType, calId, acl, k, kcount, FJQC): - from gam import ACLRuleKeyValueList if FJQC.formatJSON: if entityType == Ent.CALENDAR: if user: @@ -350,7 +389,6 @@ def _showCalendarACL(user, entityType, calId, acl, k, kcount, FJQC): printKeyValueListWithCount(ACLRuleKeyValueList(acl), k, kcount) def _infoCalendarACLs(cal, user, entityType, calId, j, jcount, ruleIds, kcount, FJQC): - from gam import formatACLScopeRole, normalizeRuleId Ind.Increment() k = 0 for ruleId in ruleIds: diff --git a/src/gam/cmd/resources.py b/src/gam/cmd/resources.py index d04fd32a..f897ba89 100644 --- a/src/gam/cmd/resources.py +++ b/src/gam/cmd/resources.py @@ -673,7 +673,7 @@ RESOURCE_FIELDS_WITH_CRS_NLS = {'resourceDescription'} def _showResource(cd, resource, i, count, FJQC, acls=None, noSelfOwner=False): - from gam import ACLRuleKeyValueList + from gam.cmd.calendar import ACLRuleKeyValueList def _showResourceField(title, resource, field): if field in resource: if field not in RESOURCE_FIELDS_WITH_CRS_NLS: diff --git a/src/gam/cmd/userservices.py b/src/gam/cmd/userservices.py index 83648c4e..85338bad 100644 --- a/src/gam/cmd/userservices.py +++ b/src/gam/cmd/userservices.py @@ -610,7 +610,7 @@ def _getCalendarAttributes(body, returnOnUnknownArgument=False): unknownArgumentExit() def _showCalendar(calendar, j, jcount, FJQC, acls=None): - from gam import ACLRuleKeyValueList + from gam.cmd.calendar import ACLRuleKeyValueList if FJQC.formatJSON: if acls: calendar['acls'] = [{'id': rule['id'], 'role': rule['role']} for rule in acls] diff --git a/src/gam/constants.py b/src/gam/constants.py index 4bb58b5d..bcc89f4a 100644 --- a/src/gam/constants.py +++ b/src/gam/constants.py @@ -146,6 +146,129 @@ YUBIKEY_VALUE_ERROR_RC = 85 YUBIKEY_MULTIPLE_CONNECTED_RC = 86 YUBIKEY_NOT_FOUND_RC = 87 + +# Boolean constants +TRUE = 'true' +FALSE = 'false' +TRUE_VALUES = [TRUE, 'on', 'yes', 'enabled', '1'] +FALSE_VALUES = [FALSE, 'off', 'no', 'disabled', '0'] +TRUE_FALSE = [TRUE, FALSE] + +# Error/warning prefixes +ERROR = 'ERROR' +ERROR_PREFIX = ERROR + ': ' +WARNING = 'WARNING' +WARNING_PREFIX = WARNING + ': ' + +# Byte sizes (powers of 10) +ONE_KILO_10_BYTES = 1000 +ONE_MEGA_10_BYTES = ONE_KILO_10_BYTES * ONE_KILO_10_BYTES +ONE_GIGA_10_BYTES = ONE_KILO_10_BYTES * ONE_MEGA_10_BYTES +ONE_TERA_10_BYTES = ONE_KILO_10_BYTES * ONE_GIGA_10_BYTES + +# Time durations in seconds +SECONDS_PER_MINUTE = 60 +SECONDS_PER_HOUR = 3600 +SECONDS_PER_DAY = 86400 +SECONDS_PER_WEEK = 604800 + +# Google limits +MAX_GOOGLE_SHEET_CELLS = 10000000 # See https://support.google.com/drive/answer/37603 +SHARED_DRIVE_MAX_FILES_FOLDERS = 500000 + +# Encoding +UTF8 = 'utf-8' +UTF8_SIG = 'utf-8-sig' + +# Environment variable names +EV_GAMCFGDIR = 'GAMCFGDIR' +EV_GAMCFGSECTION = 'GAMCFGSECTION' +EV_OLDGAMPATH = 'OLDGAMPATH' + +# Config file names +FN_GAM_CFG = 'gam.cfg' +FN_LAST_UPDATE_CHECK_TXT = 'lastupdatecheck.txt' +FN_GAMCOMMANDS_TXT = 'GamCommands.txt' + +# Drive path constants +ROOTID = 'rootid' +ORPHANS = 'Orphans' +SHARED_WITHME = 'SharedWithMe' +SHARED_DRIVES = 'SharedDrives' + +# Additional character sets +URL_SAFE_CHARS = ALPHANUMERIC_CHARS + '-._~' +FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS + "-_.() " +CHAT_MESSAGEID_CHARS = string.ascii_lowercase + string.digits + '-' + +# File mode constants +DEFAULT_CSV_READ_MODE = 'r' +DEFAULT_FILE_APPEND_MODE = 'a' +DEFAULT_FILE_READ_MODE = 'r' +DEFAULT_FILE_WRITE_MODE = 'w' + +# Application URLs +GAM_URL = f'https://github.com/{GIT_USER}/{GAM}' +GAM_RELEASES = f'https://github.com/{GIT_USER}/{GAM}/releases' +GAM_WIKI = f'https://github.com/{GIT_USER}/{GAM}/wiki' +GAM_LATEST_RELEASE = f'https://api.github.com/repos/{GIT_USER}/{GAM}/releases/latest' + +# Additional Google API MIME types +MIMETYPE_GA_DOCUMENT = f'{APPLICATION_VND_GOOGLE_APPS}document' +MIMETYPE_GA_DRAWING = f'{APPLICATION_VND_GOOGLE_APPS}drawing' +MIMETYPE_GA_FILE = f'{APPLICATION_VND_GOOGLE_APPS}file' +MIMETYPE_GA_FORM = f'{APPLICATION_VND_GOOGLE_APPS}form' +MIMETYPE_GA_FUSIONTABLE = f'{APPLICATION_VND_GOOGLE_APPS}fusiontable' +MIMETYPE_GA_JAM = f'{APPLICATION_VND_GOOGLE_APPS}jam' +MIMETYPE_GA_MAP = f'{APPLICATION_VND_GOOGLE_APPS}map' +MIMETYPE_GA_PRESENTATION = f'{APPLICATION_VND_GOOGLE_APPS}presentation' +MIMETYPE_GA_SCRIPT = f'{APPLICATION_VND_GOOGLE_APPS}script' +MIMETYPE_GA_SCRIPT_JSON = f'{APPLICATION_VND_GOOGLE_APPS}script+json' +MIMETYPE_GA_3P_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}drive-sdk' +MIMETYPE_GA_SITE = f'{APPLICATION_VND_GOOGLE_APPS}site' +MIMETYPE_GA_SPREADSHEET = f'{APPLICATION_VND_GOOGLE_APPS}spreadsheet' +MIMETYPE_TEXT_CSV = 'text/csv' +MIMETYPE_TEXT_HTML = 'text/html' +MIMETYPE_TEXT_PLAIN = 'text/plain' + +# Google infrastructure +GOOGLE_NAMESERVERS = ['8.8.8.8', '8.8.4.4'] + +# Date/time sentinel values +NEVER_DATE = '1970-01-01' +NEVER_DATETIME = '1970-01-01 00:00' +NEVER_TIME = '1970-01-01T00:00:00.000Z' +NEVER_TIME_NOMS = '1970-01-01T00:00:00Z' +NEVER_END_DATE = '1969-12-31' +NEVER_START_DATE = NEVER_DATE +REFRESH_EXPIRY = '1970-01-01T00:00:01Z' +UNKNOWN = 'Unknown' +REPLACE_GROUP_PATTERN = re.compile(r'\\(\d+)') + +# Additional Drive query fragments +MY_FOLDERS = ME_IN_OWNERS_AND + ANY_FOLDERS +WITH_ANY_FILE_NAME = "name = '{0}'" +WITH_MY_FILE_NAME = ME_IN_OWNERS_AND + WITH_ANY_FILE_NAME +WITH_OTHER_FILE_NAME = NOT_ME_IN_OWNERS_AND + WITH_ANY_FILE_NAME + +# Debug redaction patterns +DEBUG_REDACTION_PATTERNS = [ + # Positional patterns that redact sensitive credentials based on their location + (r'(Bearer\s+)\S+', r'\1*****'), # access tokens and JWTs in auth header + (r'([?&]refresh_token=)[^&]*', r'\1*****'), # refresh token URL parameter + (r'([?&]client_secret=)[^&]*', r'\1*****'), # client secret URL parameter + (r'([?&]key=)[^&]*', r'\1*****'), # API key URL parameter + (r'([?&]code=)[^&]*', r'\1*****'), # auth code URL parameter + + # Pattern match patterns that redact sensitive credentials based on known credential pattern + (r'ya29.[0-9A-Za-z-_]+', '*****'), # Access token + (r'1%2F%2F[0-9A-Za-z-_]{100}|1%2F%2F[0-9A-Za-z-_]{64}|1%2F%2F[0-9A-Za-z-_]{43}', '*****'), # Refresh token + (r'4/[0-9A-Za-z-_]+', '*****'), # Auth code + (r'GOCSPX-[0-9a-zA-Z-_]{28}', '*****'), # Client secret + (r'AIza[0-9A-Za-z-_]{35}', '*****'), # API key + (r'eyJ[a-zA-Z0-9\-_]+\.eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]*', '*****'), # JWT +] + # Building address field map BUILDING_ADDRESS_FIELD_MAP = { 'address': 'addressLines',