def get_user_json(conf: ConfParser = None): logging.info("Retriving users from Bitrix24") response = requests.get(conf.get_users_webhook_url()) file_ops.write_json_dump(response.json(), "bitrix24_all_user_data", conf.get_output_path()) return response.json()
def get_org_members(conf: ConfParser = None): """Retrieves organisation members from GitHub API.""" if conf == None or not isinstance(conf, ConfParser): logging.error("No conf given.") return auth = (conf.get_user(), conf.get_access_token()) headers = {'Accept': 'application/vnd.github.v3+json'} url = "https://api.github.com/orgs/{}/members?per_page=100".format( conf.get_organisation_name()) response = requests.get(url, headers=headers, auth=auth) return response
def main(): arguments = docopt(__doc__, version='GitHub Get Org Users 0.1') config = ConfParser() config.load_ini_conf(arguments["--config"]) # Create log file log_to_file(config, "main") # Print loaded configuration logging.info("Loaded configuration:") config.print_conf() # Retrieve organisation members org_json = get_org_members(config).json() # Grab names from the retrieved JSON login_name_dict = extract_org_member_logins(org_json) # Replace real names with value None to the real name add_names_from_file(config, login_name_dict) # Check if there are unknown users and create a dict if there are. unknown_users_dict = check_for_unknown_users(config, login_name_dict) if unknown_users_dict: json_ops.write_json_dump(unknown_users_dict, "github_unknown_users", config.get_output_path()) # Write a JSON file including known organisation members. json_ops.write_json_dump(login_name_dict, "github_known_users", config.get_output_path()) # Write a CSV file write_csv(config, login_name_dict)
def combine_users_and_departments(conf: ConfParser = None): users_json = get_user_json(conf) department_json = get_deparment_json(conf) org = dict() for department in department_json["result"]: org[department["ID"]] = { "NAME": department["NAME"], "ID": department["ID"], "head": {}, "employees": {} } # Every department doesn't have UF_HEAD or PARENT if "UF_HEAD" in department: org[department["ID"]]["UF_HEAD"] = department["UF_HEAD"] if "PARENT" in department: org[department["ID"]]["PARENT"] = department["PARENT"] parent_hierarchy = [department["NAME"]] #parent_hierarchy.append(department["NAME"]) add_parent_departments_recursively(department_json, department, parent_hierarchy) org[department["ID"]]["parent_hierarchy"] = parent_hierarchy[0] for user in users_json["result"]: if user["ACTIVE"]: for uf_dep in user["UF_DEPARTMENT"]: # If user belongs to the department, check more. # If the user is the head of the deparment, # add to head. Otherwise add to employees. if "UF_HEAD" in department and user["ID"] == department[ "UF_HEAD"]: org[department["ID"]]["head"] = { "ID": user["ID"], "EMAIL": user["EMAIL"], "NAME": user["NAME"], "LAST_NAME": user["LAST_NAME"] } if int(uf_dep) == int(department["ID"]): # The departements are not necessarily in an order that user would be added # as department head before checking if user is part of department. # Somehow in Bitrix24 one can be department head, but user_json information # doesn't show it. if "UF_HEAD" in department and user[ "ID"] == department["UF_HEAD"]: pass else: org[department["ID"]]["employees"][user["ID"]] = { "ID": user["ID"], "EMAIL": user["EMAIL"], "NAME": user["NAME"], "LAST_NAME": user["LAST_NAME"], "USER_TYPE": user["USER_TYPE"] } file_ops.write_json_dump(org, "bitrix24_org", conf.get_output_path()) return org
def log_to_file(config: ConfParser = None, log_id: str = None): log_path = config.get_log_path() log_name = log_path + log_id + "_" + \ datetime.now().strftime("%Y%m%d-%H%M%S") + ".log" # Probably some import initializes the logging already, # but logging needs to be initialized this way to get the # output to certain file. # https://stackoverflow.com/a/46098711 logging.root.handlers = [] logging.basicConfig(level=config.get_logging_level(), \ format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(log_name), logging.StreamHandler() ] )
def get_user_info(conf: ConfParser = None, username: str = None): """Retrieves user information from GitHub API.""" if conf == None or not isinstance(conf, ConfParser): logging.error("Func: {} no conf given.".format(inspect.stack()[0][3])) return if username == None or not isinstance(username, str): logging.error("Func: {} no username given.".format( inspect.stack()[0][3])) return auth = (conf.get_user(), conf.get_access_token()) headers = {'Accept': 'application/vnd.github.v3+json'} url = "https://api.github.com/users/" + username response = requests.get(url, headers=headers) return response
def __init__(self, conf: ConfParser = None): if conf is not None and isinstance(conf, ConfParser): self.conf = conf else: self.conf = ConfParser() logging.info("Creating credential token") self.drive_service = DriveService() # This variable could be flipped true and wait could be triggered, when there's error of # exceeding traffic quota. self.over_quota_bool = False # full_json stores the whole Google file structure with additional information. self.full_json = [] self.full_json_creation_error = False # If a full_json is created, the file path should be saved in this variable. self.created_full_json_path = "" # Stores file IDs and names of all checked files. self.all_checked_files_dict = {} # The user who is running the script to revoke permissions. self.current_user = self.get_current_user() # Adding special value to all_revoke_share_ids_dict, so that "empty" JSON is written out. self.all_revoke_share_ids_dict = dict({"empty_dict" : {"revoke_permissions": { "empty_permission" : {"the_only": "If this is the only one left, all other permissions have been removed."}}}}) self.num_of_revoked_permissions = 0 self.num_of_revoke_errors = 0 self.is_there_something_to_revoke = False self.parent_list = None self.parent_id_dict = None self.root_json_creation_error = False
def write_csv(conf: ConfParser = None, json_dict: dict = None): """Writes GitHub login names and real names into CSV file.""" file_out = conf.get_output_path() + "github_known_users_" + \ datetime.now().strftime("%Y%m%d-%H%M%S") + ".csv" logging.info("Writing CSV: " + file_out) with open(file_out, mode='w') as outfile: name_writer = csv.writer(outfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) name_writer.writerow(['GitHub login', 'Name']) for user, real_name in json_dict.items(): name_writer.writerow([user, real_name])
def main(): arguments = docopt(__doc__, version='Bitrix24 Get Users 0.1') config = ConfParser() config.load_ini_conf(arguments["--config"]) # Create log file log_to_file(config, "bitrix24_get_users") # Print loaded configuration logging.info("Loaded configuration:\n" + config.get_conf_str()) logging.info("Combining users with departments") org = combine_users_and_departments(config) file_ops.write_csv_org(org, "org", config.get_output_path())
def add_names_from_file(conf: ConfParser = None, json_dict: dict = None): """Combines a real name with the GitHub login name. Uses a user created JSON file as an information source.""" if conf == None or not isinstance(conf, ConfParser): logging.error("Func: {} no conf given.".format(inspect.stack()[0][3])) return if json_dict == None or not isinstance(json_dict, dict): logging.error("Func: {} no json_dict given.".format( inspect.stack()[0][3])) return real_names_dict = json_ops.open_json(conf.get_real_names_json()) # Loop trough the json_dict (which contains login usernames retrieved from GitHub) # and insert real names to the dict value. for login_name_json in json_dict.keys(): for login_name_file, real_name in real_names_dict.items(): if login_name_json == login_name_file: json_dict[login_name_json] = real_name
def check_for_unknown_users(conf: ConfParser = None, json_dict: dict = None): """Checks if there are any unkown users and creates a dictionary of the unknown users.""" if conf == None or not isinstance(conf, ConfParser): logging.error("Func: {} no conf given.".format(inspect.stack()[0][3])) return if json_dict == None or not isinstance(json_dict, dict): logging.error("Func: {} no json_dict given.".format( inspect.stack()[0][3])) return real_names_dict = json_ops.open_json(conf.get_real_names_json()) unknown_users_dict = dict() for login_name_json, val in json_dict.items(): if not login_name_json in real_names_dict: logging.warning("No login name {} found!".format(login_name_json)) unknown_users_dict[login_name_json] = val return unknown_users_dict
class PermissionRevoker: """A class for revoking Google Drive user/domain permission from files.""" def __init__(self, conf: ConfParser = None): if conf is not None and isinstance(conf, ConfParser): self.conf = conf else: self.conf = ConfParser() logging.info("Creating credential token") self.drive_service = DriveService() # This variable could be flipped true and wait could be triggered, when there's error of # exceeding traffic quota. self.over_quota_bool = False # full_json stores the whole Google file structure with additional information. self.full_json = [] self.full_json_creation_error = False # If a full_json is created, the file path should be saved in this variable. self.created_full_json_path = "" # Stores file IDs and names of all checked files. self.all_checked_files_dict = {} # The user who is running the script to revoke permissions. self.current_user = self.get_current_user() # Adding special value to all_revoke_share_ids_dict, so that "empty" JSON is written out. self.all_revoke_share_ids_dict = dict({"empty_dict" : {"revoke_permissions": { "empty_permission" : {"the_only": "If this is the only one left, all other permissions have been removed."}}}}) self.num_of_revoked_permissions = 0 self.num_of_revoke_errors = 0 self.is_there_something_to_revoke = False self.parent_list = None self.parent_id_dict = None self.root_json_creation_error = False def create_full_json(self, parent_id: str = None): """Creates a full_json JSON file and saves to disk. Creating a full_json file should be done as r Parameters ---------- Returns ------- list A list in JSON syntax. Content is all relevant information about Google Drive files/folders for revoking shares. """ conf = self.conf service = self.drive_service.service if not parent_id or not isinstance(parent_id, str): parent_id = self.conf.get_parent_id() logging.info("### Creating full_json") logging.info("Retrieving parent folder as JSON") # Creating a list of so it's easy loop the files afterwards. self.full_json = [self.get_json_of_one_google_file(parent_id)] # Check if full_json is valid and there was no error. try: if self.full_json[0]["error"]["message"]: self.full_json_creation_error = True return except KeyError: pass logging.info("Adding children recursively") self.add_children_recursively(self.full_json) logging.info("All files in the retrieved list:") self.log_all_files(self.full_json) logging.info("Total number of files/folders including root folder: " + str(len(self.all_checked_files_dict))) logging.info("Saving full JSON file") full_json_path = self.write_json_dump(self.full_json, parent_id) logging.info("### Finished full_json creation") # Set the conf to match path of full_json self.created_full_json_path = full_json_path def create_parent_dict(self): """Creates a list of parent IDs which then can be used to create full_jsons and revoke permissions.""" logging.info("### Creating parent_id list") logging.info("Retrieving root folder as JSON") root_json = [self.get_json_of_one_google_file(self.conf.get_root_id())] # Check if root_json is valid and there was no error. try: if root_json[0]["error"]["message"]: self.root_json_creation_error = True return except KeyError: pass logging.info("Adding one level of children to root JSON") # Arguments: JSON, max_depth == how many leves, current_depth == start with 0 # JSON is mandatory, but depths are not. If depths are not given the whole file tree under # the parent will be checked. self.add_children_recursively(root_json, 1, 0) logging.info("All files in the retrieved list:") parent_id_dict = dict() # Create one item to the dict, so it's not empty parent_id_dict["empty"] = {"name":"empty", "folder":False} # log_all_files adds all checked files to parent_id_dict self.log_all_files(root_json, parent_id_dict, True) # Remove the "empty" item del parent_id_dict["empty"] logging.info("Total number of files/folders including root folder: " + str(len(parent_id_dict))) logging.info("Saving root JSON file") root_json_path = self.write_json_dump(root_json, self.conf.get_root_id()) logging.info("### Finished root_json creation") parent_dict_json_path = self.write_json_dump(parent_id_dict, "parent_id_dict_" + self.conf.get_root_id()) self.parent_id_dict = parent_id_dict def get_json_of_one_google_file(self, gfile: str = None): # gfile == Google Drive file/folder ID # For example: # https://drive.google.com/drive/folders/16mbbT12343ypAs81234sGyO3u9fgHkkp service = self.drive_service.service if not gfile and isinstance(gfile, str): logging.error("Func: {}, no Google Folder/File ID given.".format(inspect.stack()[0][3])) return if not service and isinstance(service, Resource): logging.error("Func: {}, no service object given.".format(inspect.stack()[0][3])) return # These fields are not retrieved if requesting user is not owner/editor, so it's important # to retrieve owners/me field. # permissions # permissionIds # sharingUser # # If user is not owner, capabilities field can determine if user is # commenter, editor or viewer. # canEdit => editor # canComment => commenter # cannot comment or edit => viewer # # cababilities/canShare == determines if the current user can revoke permissions. # Neither editor without sharing cabaility, commenter or viewer can revoke permissions. # # try-except is required, because there will be error if shortcut's targetId is # unavailable. try: response = service.files().get(fileId=gfile, \ fields='id, \ name, \ parents, \ mimeType, \ kind, \ createdTime, \ modifiedTime, \ shared, \ shortcutDetails, \ permissions, \ owners/me, \ owners/displayName, \ owners/permissionId, \ owners/emailAddress, \ capabilities/canShare').execute() logging.info('Retrieved JSON data of: {} {}'.format(response['id'], response['name'])) except HttpError as err: response = json.loads(err.content) logging.error(response["error"]["message"]) return response finally: pass # Return the parent in a list as then other functions work for both parent # and children. return response def add_children_recursively(self, json_file: list = None, max_depth: int = None, cur_depth: int = None): """Recursive function, which sdds children files/folders to parent JSON. The function uses another get_children_as_json(). Parameters ---------- list JSON file. Starting JSON file has to be created with get_json_of_one_google_file(). """ service = self.drive_service.service # Check arguments if not json_file or not isinstance(json_file, list): logging.error("Func: {}, no JSON given.".format(inspect.stack()[0][3])) return #if not json_file or not isinstance(json_file, list): # logging.error("Func: {}, no JSON given.".format(inspect.stack()[0][3])) # return # gfile == Google file/folder ID for gfile in json_file: # Check that mimeType is folder if "mimeType" in gfile and gfile["mimeType"] == "application/vnd.google-apps.folder": # Check for depth logging.debug("Depths cur_depth {} max_depth {}".format(cur_depth, max_depth)) # If no max_depth was given, we can traverse freely all children. # If max_depth was given, as long as its smaller than cur_depth we can continue. if max_depth and cur_depth and cur_depth >= max_depth: logging.warning("There's still children files/folders, but max_depth" + " was reached, so retrieving children was stopped.") return # Add 1 to the current depth if cur_depth was given, meaning it's not None. if not cur_depth is None: cur_depth += 1 logging.info("Adding children of: {} {}".format(gfile["id"], gfile["name"])) # Add children to the JSON file gfile["children"] = self.get_children_json(gfile["id"]) # Check if returned gfile["children"] exists and continue traversing recursively # if not empty. If max_depth is zero, don't traverse. if gfile["children"]: # Call the function recursively with the added children list self.add_children_recursively(gfile["children"], max_depth, cur_depth) else: logging.info(" Not a folder: {} {} {}".format( gfile["id"], gfile["name"], gfile["mimeType"])) def get_children_json(self, gfile: str = None): # Check arguments if not gfile or not isinstance(gfile, str): logging.error("Func: {}, no JSON given.".format(inspect.stack()[0][3])) return service = self.drive_service.service # Replace the PARENT_ID between single quotes with the real parent_id given query = re.sub("PARENT_ID", gfile, "'PARENT_ID' in parents") # Resources: # https://developers.google.com/drive/api/v3/reference/files # Example for retrieving paged information: # https://developers.google.com/drive/api/v3/search-files page_token = None all_responses = [] # These fields are not retrieved if requesting user is not owner/editor, so it's important # to retrieve owners/me field. # permissions # permissionIds # sharingUser # # If user is not owner, capabilities field can determine if user is # commenter, editor or viewer. # canEdit => editor # canComment => commenter # cannot comment or edit => viewer # # cababilities/canShare == determines if the current user can revoke permissions. # Neither editor without sharing cabaility, commenter or viewer can revoke permissions. while True: response = service.files().list(q=query, pageSize=1000, spaces='drive', fields='nextPageToken, \ files(id, \ name, \ parents, \ mimeType, \ kind, \ createdTime, \ modifiedTime, \ shared, \ shortcutDetails, \ permissions, \ owners/me, \ owners/displayName, \ owners/permissionId, \ owners/emailAddress, \ capabilities/canShare)', \ pageToken=page_token).execute() for f in response.get('files', []): # Process change logging.info(' Found a child: {} {}'.format(f['name'], f['id'])) all_responses += response.get('files', []) page_token = response.get('nextPageToken', None) if page_token is None: break # Requires deepcopy, so changes can be made during looping. children_json = copy.deepcopy(all_responses) # Check if there's shortucs and if there are, add their targets. for gfile in all_responses: if "mimeType" in gfile and \ gfile["mimeType"] == "application/vnd.google-apps.shortcut": shortcut_target = self.get_json_of_one_google_file(gfile["shortcutDetails"]["targetId"]) # Check if there was error "File not found". # find return -1 if substring not found. if "error" in shortcut_target and \ shortcut_target["error"]["message"].find("File not found") != -1: logging.warning(" Probably shortcut's targetId file/folder has been " + "either removed or current user doesn't have access to the file/folder.") else: children_json.append(shortcut_target) return children_json def create_all_revoke_share_ids(self, json_file: list = None): """Recursive function. Retrieves all fileIDs of shared fileas and the permissionIDs which have access. Parameters ---------- json_file : list A JSON file which has Google Drive file/folder structure information. """ # Checks that arguments are correct. if not json_file or not isinstance(json_file, list): logging.error("Func: {}, no JSON given.".format(inspect.stack()[0][3])) return # gfile means Google file/folder for gfile in json_file: # Check that there's share section in gfile and check it's true if "shared" in gfile and gfile["shared"]: # If the current user is NOT owner or editor with canShare capability, # the current user is not authorized to revoke shares. # Only owner or editor with sharing capability can revoke shares. # If there's no canShare capability, user is either editor without sharing # permission or reader/commenter. # reader/commenter can't revoke revoke a share. if not gfile["capabilities"]["canShare"]: # Notice that can't use "continue" as the recursive part in the end of function # would be skipped. pass else: # Temp permission dict for storing to be revoked permissions tmp_perms = {gfile["id"] : { "name" : gfile["name"], "owner.me" : gfile["owners"][0]["me"], "revoke_permissions" : {} }} for permission in gfile["permissions"]: # Don't try to revoke permission of an owner, # that would be just waste of traffic quota. # Domain doesn't have "deleted" property, so it needs to be excepted. # The folders show "shared person symbol" even if the Web GUI doesn't show # any share, so probably "deleted" shares need to be revoked also. if permission["role"] != "owner" or permission["type"] == "domain": logging.debug(" FileID: {} has permissionId: {}".format(gfile["id"], permission["id"])) # Domain share doesn't have same properties as real users if permission["type"] == "domain": tmp_perms[gfile["id"]]["revoke_permissions"].update( {permission["id"] : {"domain": permission["domain"], "role" : permission["role"]} }) # "anyone" type is special as the permissionId is "anyoneWithLink" elif permission["type"] == "anyone": tmp_perms[gfile["id"]]["revoke_permissions"].update( {permission["id"] : {"role" : permission["role"]}}) else: tmp_perms[gfile["id"]]["revoke_permissions"].update( { permission["id"] : {"emailAddress" : permission["emailAddress"], "role" : permission["role"], "deleted" : permission["deleted"] } }) # If some permissions were added to the temp file, add it to # the real revoking dict if tmp_perms[gfile["id"]]["revoke_permissions"]: self.all_revoke_share_ids_dict.update(tmp_perms) self.is_there_something_to_revoke = True # Call the function recursively with children list if "children" in gfile and gfile["children"]: logging.debug("Checking folder ID: {}".format(gfile["id"])) self.create_all_revoke_share_ids(gfile["children"]) def log_files_to_be_revoked(self): """Logs file IDs and file names that have permissions which are possible to revoke.""" tmp_rev_dict = self.all_revoke_share_ids_dict if tmp_rev_dict: tmp_file_ids_names = "" for file_id, val_dict in tmp_rev_dict.items(): if file_id != "empty_dict": tmp_file_ids_names += "\n" + file_id + " \"" + val_dict["name"] + "\"" logging.info("### Files that have permissions which are possible to revoke:" + " (syntax: FileID \"File Name\" ): {}".format(tmp_file_ids_names)) else: logging.info("### No files to revoke!") def log_all_files(self, json_file: list = None, checked_files: dict = None, checking_root: bool = None): """Recursive function which prints and logs all children. Parameters ---------- json_file : list A JSON file to check children from. checked_files : dict A dictionary which contains the checked files/folders. checking_root : bool If a parent ID dict was created, there's no need to print 'No children' info. """ if checked_files == None: all_dict = self.all_checked_files_dict else: all_dict = checked_files if checking_root == None: checking_root = False # Check arguments if not json_file or not isinstance(json_file, list): logging.error("Func: {}, no JSON given.".format(inspect.stack()[0][3])) return if all_dict is None or not isinstance(all_dict, dict): logging.error("Func: {}, no dict given.".format(inspect.stack()[0][3])) return for gfile in json_file: # The root should be always folder. Otherwise no reason to log # children. if "mimeType" in gfile and gfile["mimeType"] == "application/vnd.google-apps.folder": # Log found folder logging.info(" {} {}".format(gfile["id"], gfile["name"])) # Add folders to all_dict all_dict[gfile["id"]] = {"name": gfile["name"], "folder": True} # Call the function recursively with children list # if there's key "children" and it's not empty list. if "children" in gfile and gfile["children"]: logging.info(" id: {} children: ".format(gfile["id"])) self.log_all_files(gfile["children"], all_dict, checking_root) elif checking_root: pass else: logging.info(" No children id: {}".format(gfile["id"])) else: logging.info(" id: {} name: {}".format(gfile["id"], gfile["name"])) # Add files to all_dict all_dict[gfile["id"]] = {"name": gfile["name"], "folder": False} def open_json(self, file_name: str = None): """Opens a JSON file. Parameters ---------- str To be opened file with path. """ if not file_name or not isinstance(file_name, str): logging.error("Func: {}, no file name given.".format(inspect.stack()[0][3])) return logging.info("Opening JSON file: " + file_name) json_file = "" try: with open(file_name, 'r') as f: json_file = json.load(f) except (IOError, IsADirectoryError) as e: logging.error(e) return json_file def get_json_dump(self, json_obj_file): """Function for creating pretty print JSON.""" return json.dumps(json_obj_file, indent=4, ensure_ascii=False) def print_json_dump(self, json_obj: list = None): """Function for testing console prints.""" print(json.dumps(json_obj, indent=4, ensure_ascii=False)) def write_json_dump(self, json_obj: list = None, file_name: str = None): """Write a JSON dump to a given folder. Parameters ---------- json_obj : list file_name : str Returns ------- str Output dir + filename """ output_path = self.conf.get_output_path() if not json_obj: logging.error("Func: {}, no JSON given.".format(inspect.stack()[0][3])) return # If no file name given, just use "no_name_given" as prefix. if not file_name: logging.warning("Func: {}, no file name given.".format(inspect.stack()[0][3])) file_name = "no_name_given_" file_out = output_path + file_name + "_" + \ datetime.now().strftime("%Y%m%d-%H%M%S") + ".json" logging.info(" Writing JSON: " + file_out) with open(file_out, 'w') as outfile: json.dump(json_obj, outfile, indent=4, ensure_ascii=False) # Return the full path + file name return file_out def get_current_user(self): """Retrieves current user information from Google Drive API.""" logging.info("Checking current user information") response = self.drive_service.service.about().get(fields='user').execute() logging.info(' Current user: {} permissionId: {}'.format( response["user"]["emailAddress"], response["user"]["permissionId"])) return response def wait_print(self, secs: int = None): """Function for making the program to wait between API calls, so that the traffic quota would not be exceeded.""" if not secs: return secs = secs + 1 for sec in range(1, secs): # flush=True, so that the time is printed. print(sec, end =" ", flush=True) time.sleep(1) print("\n") def revoke_one_file_permission(self, gfile: str = None, permission_id: str = None): """Revokes one share from one file/folder. Returns ------- list JSON response if successful or not. Empty JSON means that the revoke was successful. """ service = self.drive_service.service if not gfile or not isinstance(gfile, str): logging.error("Func: {}, no fileId given.".format(inspect.stack()[0][3])) return if not permission_id or not isinstance(permission_id, str): logging.error("Func: {}, no permissionId given.".format(inspect.stack()[0][3])) return logging.info('Revoking permission: FileId {} permissionId {}'.format(gfile, permission_id)) try: # response is 204 without content if successful # This can be tested with: # https://developers.google.com/drive/api/v3/reference/permissions/delete response = service.permissions().delete(fileId=gfile, \ permissionId=permission_id).execute() logging.info(' Successfully revoked permission: FileId {} permissionId {}'.format( gfile, permission_id)) self.num_of_revoked_permissions += 1 except HttpError as err: # https://stackoverflow.com/questions/23945784/how-to-manage-google-api-errors-in-python response = json.loads(err.content) logging.error(response["error"]["message"]) self.num_of_revoke_errors += 1 finally: pass # If response is empty, the request was successful. return response def valid_for_revoke(self): """A helper function to check if permission revoking can start.""" rev_files = None # If all_revoke_share_ids_dict has been initialized during this program run, # use the initialized all_revoke_share_ids_dict. Otherwise load from file. if not self.is_there_something_to_revoke: return # There's actually always at least 1 item in the dict elif len(self.all_revoke_share_ids_dict) > 1: rev_files = self.all_revoke_share_ids_dict #rev_files = self.open_json(self.conf.get_revoke_json_path()) return rev_files def revocations_were_made(self, tmp_rev_dict: dict = None, file_name: str = None): """Helper function to process dict if revocations were made.""" if not isinstance(tmp_rev_dict, dict): logging.error("Func: {}, no dict given.".format(inspect.stack()[0][3])) return ## Have to create a deepcopy for deleting keys. rm_dict = copy.deepcopy(tmp_rev_dict) # Loop through the dictionary and remove any empty permissions for gfile in tmp_rev_dict: if not tmp_rev_dict[gfile]["revoke_permissions"]: logging.debug("Removing permissions section from dict as there are no " + "permissions anymore in the section.") del rm_dict[gfile] # Point self.all_revoke_share_ids_dict to the changed/updated dict. self.all_revoke_share_ids_dict = rm_dict # If everything is unshared tmp_rev_dict, should have only 1 index. if len(self.all_revoke_share_ids_dict) == 1: logging.info("All possible shares should have been revoked!") self.write_json_dump(rm_dict, file_name + self.conf.get_parent_id()) def handle_revoke_response(self, revoke_response: dict = None, permission: str = None, gfile: str = None, tmp_rev_dict: dict = None): """A helper function for handling revocation responses.""" # Empty revoke_response means that revocation was successful. if not revoke_response: # Removing permission from dict because the revocation request was successful. del tmp_rev_dict[gfile]["revoke_permissions"][permission] # find returns -1 if substring not found. # This should check if known errors are found and remove gfile from tmp_rev_dict # if known error found. elif revoke_response and \ revoke_response["error"]["message"].find("Permission not found") != -1 or \ revoke_response["error"]["message"].find("File not found") != -1: # Those errors mean that the permission doesn't exist anymore, so the permission # can be removed from dict also. del tmp_rev_dict[gfile]["revoke_permissions"][permission] else: logging.error("Unhandled revoking error: " + revoke_response["error"]["message"]) def revoke_deleted(self): rev_files = self.valid_for_revoke() # If there the validator function didn't return anything. if rev_files is None: logging.error("Func: {}, no JSON to revoke!".format(inspect.stack()[0][3])) return # Requires a deepcopy, otherwise there's error "dictionary changed size during iteration" tmp_rev_dict = copy.deepcopy(rev_files) # Current user information cur_user_perm = self.current_user["user"]["permissionId"] # This is just a temp variable to track if something was revoked/"attempted to revoke". # If nothing is changed, there's no need to write out an unchanged JSON. revoke_attempt = False # gfile == Google Drive file/folder's hash ID. # rev_files is a dict which keys are Google Drive file/folder hash IDs. # rev_files has been created with func create_all_revoke_share_ids() for gfile in rev_files: # info == information about the permission: user, domain, email, deleted, etc for permission, info in rev_files[gfile]["revoke_permissions"].items(): # Revoke permissions from deleted users. Google Drive WebUI will show sharing icon # otherwise even if the file/folder doesn't have active shares. if "deleted" in info.keys() and info["deleted"]: revoke_attempt = True logging.debug("Revoking a permission of a deleted user: "******" " + rev_files[gfile]["name"]) revoke_response = self.revoke_one_file_permission(gfile, permission) # If revoke_response is empty, the revocation was successful. self.handle_revoke_response(revoke_response, permission, gfile, tmp_rev_dict) # If revocations were made, remove the permission from dict. if revoke_attempt: self.revocations_were_made(tmp_rev_dict, "revoked_deleted_") def revoke_email_list(self): rev_files = self.valid_for_revoke() # If there the validator function didn't return anything. if rev_files is None: logging.error("Func: {}, no JSON to revoke!".format(inspect.stack()[0][3])) return # Requires a deepcopy, otherwise there's error "dictionary changed size during iteration" tmp_rev_dict = copy.deepcopy(rev_files) # Current user information cur_user_perm = self.current_user["user"]["permissionId"] # This is just a temp variable to track if something was revoked/"attempted to revoke". # If nothing is changed, there's no need to write out an unchanged JSON. revoke_attempt = False # Retrieve information from configuration revoke_email_list = self.conf.get_revoke_email_list() # gfile == Google Drive file/folder's hash ID. # rev_files is a dict which keys are Google Drive file/folder hash IDs. # rev_files has been created with func create_all_revoke_share_ids() for gfile in rev_files: # info == information about the permission: user, domain, email, deleted, etc for permission, info in rev_files[gfile]["revoke_permissions"].items(): # Revoke email addresses defined in configuration file # Don't revoke current user if permission != cur_user_perm and "emailAddress" in info.keys(): for email in revoke_email_list: # permission in tmp_rev_dict[gfile]["revoke_permissions"] check is required # so that it's known if the permissions has been revoked already. For example # there could be multiple times same email address in config file. if info["emailAddress"].lower() == email.lower() and \ permission in tmp_rev_dict[gfile]["revoke_permissions"]: revoke_attempt = True logging.debug("Revoking " + info["emailAddress"] + "from file: " + gfile + " " + rev_files[gfile]["name"]) revoke_response = self.revoke_one_file_permission(gfile, permission) # If revoke_response is empty, the revocation was successful. self.handle_revoke_response(revoke_response, permission, gfile, tmp_rev_dict) # Break out from the loop, because one email address can't have # multiple different permissions on one file. break # If revocations were made, remove the permission from dict. if revoke_attempt: self.revocations_were_made(tmp_rev_dict, "revoked_emails_") def revoke_email_domain_list(self): rev_files = self.valid_for_revoke() # If there the validator function didn't return anything. if rev_files is None: logging.error("Func: {}, no JSON to revoke!".format(inspect.stack()[0][3])) return # Requires a deepcopy, otherwise there's error "dictionary changed size during iteration" tmp_rev_dict = copy.deepcopy(rev_files) # Current user information cur_user_perm = self.current_user["user"]["permissionId"] # This is just a temp variable to track if something was revoked/"attempted to revoke". # If nothing is changed, there's no need to write out an unchanged JSON. revoke_attempt = False # Retrieve information from configuration revoke_email_domain_list = self.conf.get_revoke_email_domain_list() # gfile == Google Drive file/folder's hash ID. # rev_files is a dict which keys are Google Drive file/folder hash IDs. # rev_files has been created with func create_all_revoke_share_ids() for gfile in rev_files: # info == information about the permission: user, domain, email, deleted, etc for permission, info in rev_files[gfile]["revoke_permissions"].items(): # Revoke email domains defined in configuration file # Don't revoke current user if permission != cur_user_perm and "emailAddress" in info.keys(): for email_d in revoke_email_domain_list: if info["emailAddress"].lstrip().split('@')[1].lower() == email_d.lower() and \ permission in tmp_rev_dict[gfile]["revoke_permissions"]: revoke_attempt = True logging.debug("Revoking {} with email domain {} from file {}".format( permission, email_d, rev_files[gfile]["name"])) revoke_response = self.revoke_one_file_permission(gfile, permission) # If revoke_response is empty, the revocation was successful. self.handle_revoke_response(revoke_response, permission, gfile, tmp_rev_dict) # If revocations were made, remove the permission from dict. if revoke_attempt: self.revocations_were_made(tmp_rev_dict, "revoked_email_domains_") def revoke_permission_list(self): rev_files = self.valid_for_revoke() # If there the validator function didn't return anything. if rev_files is None: logging.error("Func: {}, no JSON to revoke!".format(inspect.stack()[0][3])) return # Requires a deepcopy, otherwise there's error "dictionary changed size during iteration" tmp_rev_dict = copy.deepcopy(rev_files) # Current user information cur_user_perm = self.current_user["user"]["permissionId"] # This is just a temp variable to track if something was revoked/"attempted to revoke". # If nothing is changed, there's no need to write out an unchanged JSON. revoke_attempt = False # Retrieve information from configuration revoke_permission_list = self.conf.get_revoke_permission_id_list() # gfile == Google Drive file/folder's hash ID. # rev_files is a dict which keys are Google Drive file/folder hash IDs. # rev_files has been created with func create_all_revoke_share_ids() for gfile in rev_files: # info == information about the permission: user, domain, email, deleted, etc for permission, info in rev_files[gfile]["revoke_permissions"].items(): # Revoke permissions defined in configuration file. # Don't revoke current user. if permission != cur_user_perm and \ permission in tmp_rev_dict[gfile]["revoke_permissions"]: # Loop through the permissions that are wanted to be revoked and # check if current permissions is on of them. for rev_perm in revoke_permission_list: if permission == rev_perm: revoke_attempt = True logging.debug("Revoking {} from file {} {}".format( permission, gfile, rev_files[gfile]["name"])) revoke_response = self.revoke_one_file_permission(gfile, permission) # If revoke_response is empty, the revocation was successful. self.handle_revoke_response(revoke_response, permission, gfile, tmp_rev_dict) # Check if the permission was domain and that was the only way for # current user to revoke permissions. If the permission was from domain # and current user doesn't have any permissions for the file anymore, # the rest of the permissions should be removed from rev_files dict. cur_user_has_still_access = False if tmp_rev_dict[gfile]["owner.me"]: cur_user_has_still_access = True # Check that there are still permissions to revoke. if tmp_rev_dict[gfile]["revoke_permissions"] and \ not cur_user_has_still_access: # If the revoked permission was domain or anyoneWithLink and role # was writer/editor, check that current user stil has revoker access # to the Google file. if "domain" in info or permission.lower() == "anyonewithlink" and \ info["role"] == "writer": # Get the current user's Google domain. cur_user_email = self.current_user["user"]["emailAddress"] cur_user_domain = cur_user_email.lstrip().split('@')[1].lower() # Temp permission, temp info. The permission inspected above # has been deleted from tmp_rev_dict, so tmp_rev_dict needs to # be looped for obsolete gfiles/permissions. for tmp_p, tmp_i in \ tmp_rev_dict[gfile]["revoke_permissions"].items(): if tmp_p == cur_user_perm and tmp_i["role"] == "writer": cur_user_has_still_access = True # Current user has still writer role, so let's break. break # Check if the domain is same as current user and writer # role. elif "domain" in tmp_i and \ tmp_i["domain"] == cur_user_domain and \ tmp_i["role"] == "writer": cur_user_has_still_access = True break # Check if anyoneWithLink has writer/editor role. elif tmp_p.lower() == "anyonewithlink" and \ tmp_i["role"] == "writer": cur_user_has_still_access = True break # If current user doesn't have access anymore, remove obsolete # files/permissions from revocation dict. if not cur_user_has_still_access: logging.debug(" Current user does not have revocation" + " access anymore for file: {}".format(gfile)) del tmp_rev_dict[gfile] else: logging.debug(" Current user has still revocation access" + " for file: {}".format(gfile)) # Break out from the loop, because one file can't have multiple same # permissions. break # If revocations were made, remove the permission from dict. if revoke_attempt: self.revocations_were_made(tmp_rev_dict, "revoked_permissions_") def revoke_current_user(self): rev_files = self.valid_for_revoke() # If there the validator function didn't return anything. if rev_files is None: logging.error("Func: {}, no JSON to revoke!".format(inspect.stack()[0][3])) return # Requires a deepcopy, otherwise there's error "dictionary changed size during iteration" tmp_rev_dict = copy.deepcopy(rev_files) # Current user information cur_user_perm = self.current_user["user"]["permissionId"] # This is just a temp variable to track if something was revoked/"attempted to revoke". # If nothing is changed, there's no need to write out an unchanged JSON. revoke_attempt = False # gfile == Google Drive file/folder's hash ID. # rev_files is a dict which keys are Google Drive file/folder hash IDs. # rev_files has been created with func create_all_revoke_share_ids() for gfile in rev_files: # info == information about the permission: user, domain, email, deleted, etc for permission, info in rev_files[gfile]["revoke_permissions"].items(): # Revoke current user if not owner. if permission == cur_user_perm and \ permission in tmp_rev_dict[gfile]["revoke_permissions"] and \ not tmp_rev_dict[gfile]["owner.me"]: revoke_attempt = True logging.debug("Revoking {} from file {} {}".format( permission, gfile, rev_files[gfile]["name"])) revoke_response = self.revoke_one_file_permission(gfile, permission) # If revoke_response is empty, the revocation was successful. self.handle_revoke_response(revoke_response, permission, gfile, tmp_rev_dict) # If revocations were made, remove the permission from dict. if revoke_attempt: self.revocations_were_made(tmp_rev_dict, "revoked_current_user_") def revoke_all_except_current_user(self): rev_files = self.valid_for_revoke() # If there the validator function didn't return anything. if rev_files is None: logging.error("Func: {}, no JSON to revoke!".format(inspect.stack()[0][3])) return # Requires a deepcopy, otherwise there's error "dictionary changed size during iteration" tmp_rev_dict = copy.deepcopy(rev_files) # Current user information cur_user_perm = self.current_user["user"]["permissionId"] # This is just a temp variable to track if something was revoked/"attempted to revoke". # If nothing is changed, there's no need to write out an unchanged JSON. revoke_attempt = False # gfile == Google Drive file/folder's hash ID. # rev_files is a dict which keys are Google Drive file/folder hash IDs. # rev_files has been created with func create_all_revoke_share_ids() for gfile in rev_files: # info == information about the permission: user, domain, email, deleted, etc for permission, info in rev_files[gfile]["revoke_permissions"].items(): # Revoke all except current user. # # Check that the gfile is not "empty_dict", dict key "empty_dict" is required, # so that the dict doesn't become completely empty. if permission != cur_user_perm and \ permission in tmp_rev_dict[gfile]["revoke_permissions"] and \ gfile != "empty_dict": revoke_attempt = True logging.debug("Revoking {} from file {} {}".format( permission, gfile, rev_files[gfile]["name"])) revoke_response = self.revoke_one_file_permission(gfile, permission) # If revoke_response is empty, the revocation was successful. self.handle_revoke_response(revoke_response, permission, gfile, tmp_rev_dict) # If revocations were made, remove the permission from dict. if revoke_attempt: self.revocations_were_made(tmp_rev_dict, "revoked_all_except_current_user_")
def main(): arguments = docopt(__doc__, version='Unshare Google Drive 0.1') config = ConfParser() config.load_ini_conf(arguments["--config"]) # Create log file log_to_file(config, config.get_parent_id()) logging.info("Loaded configuration: \n" + config.get_conf_str()) revoker = PermissionRevoker(config) create_json = config.get_create_json_bool() revoke_with_root_id = config.get_revoke_with_root_id_bool() revoke_with_json = config.get_revoke_with_json_bool() # This is checked in function revoke_id() before revocation operation. revoke_nothing = config.get_revoke_nothing_bool() if revoke_nothing: logging.warning( "Configuration file has setting revoke_nothing = True, nothing will be revoked." ) # Check that only one of the variables above is True as otherwise there's # conflict. if sum([create_json, revoke_with_root_id, revoke_with_json]) > 1: logging.error( "There is a conflict in the configuration file.\n" + "Only one of the options create_json, revoke_with_root_id, and revoke_with_json can be True." ) if revoke_with_root_id: revoker.create_parent_dict() # Check if parent dict creation was successfull. if not revoker.root_json_creation_error: # Create copy and delete root_id. tmp_parent_dict = copy.deepcopy(revoker.parent_id_dict) tmp_parent_dict.pop(config.get_root_id(), None) # Iterate through the parent ID dictionary and revoke permissions. for k in tmp_parent_dict: revoke_id(config, k) else: logging.error( "Creation of parent ID list failed. Check previous error from logs." ) elif create_json: config.revoke_nothing = True revoke_id(config, config.get_parent_id()) elif revoke_with_json: pass return
def revoke_id(conf: ConfParser = None, parent_id: str = None): """This function revokes permissions of the parent_id and it's children.""" # Check args if not conf or not isinstance(conf, ConfParser): logging.error("Func: {}, no conf given.".format(inspect.stack()[0][3])) return if not parent_id or not isinstance(parent_id, str): logging.error("Func: {}, no parent_id given.".format( inspect.stack()[0][3])) return logging.info("### BEGIN logging revocation of parent ID: " + parent_id) tmp_revoker = PermissionRevoker(conf) tmp_revoker.create_full_json(parent_id) if tmp_revoker.full_json_creation_error == True: logging.error("Something went wrong with retrieving Google Drive" \ + " file structure.") return logging.info("### Get files to be revoked") # Revoke with a previously created JSON if determined in configuration # file and not revoking with root_id. if conf.get_revoke_with_json_bool( ) and not conf.get_revoke_with_root_id_bool: tmp_revoker.all_revoke_share_ids_dict = tmp_revoker.open_json( conf.get_revoke_json_path()) else: # Go through full_json and create a dict of permissions possible to be revoked. # create_all_revoke_share_ids is recursive function. tmp_revoker.create_all_revoke_share_ids(tmp_revoker.full_json) tmp_revoker.write_json_dump(tmp_revoker.all_revoke_share_ids_dict, "possible_to_revoke" \ + tmp_revoker.conf.get_parent_id()) # -1 because the dict has 1 item when the dict is initialized. logging.info("Amount of possible permissions to be revoked: " + \ str(len(tmp_revoker.all_revoke_share_ids_dict)-1)) if len(tmp_revoker.all_revoke_share_ids_dict) > 1: tmp_revoker.is_there_something_to_revoke = True tmp_revoker.log_files_to_be_revoked() # Don't try to revoke if there's nothing to revoke. if tmp_revoker.is_there_something_to_revoke and not conf.get_revoke_nothing_bool( ): # Revoke permissions if they're set to be revoked. if conf.get_revoke_deleted_bool(): tmp_revoker.revoke_deleted() if conf.get_revoke_email_domains_bool(): tmp_revoker.revoke_email_domain_list() if conf.get_revoke_permissions_bool(): tmp_revoker.revoke_permission_list() if conf.get_revoke_current_user_bool(): tmp_revoker.revoke_current_user() if conf.get_revoke_emails_bool(): tmp_revoker.revoke_email_list() if conf.get_revoke_all_except_current_user_bool(): tmp_revoker.revoke_all_except_current_user() # Print the path to new JSON file on the screen. # This should be one of the last things to print as it's needed to configure # configuration file. logging.info("The newly created full JSON: " + tmp_revoker.created_full_json_path) logging.info("Number of revoke erros: " + str(tmp_revoker.num_of_revoke_errors)) logging.info("Number of revoked permissions: " + str(tmp_revoker.num_of_revoked_permissions)) logging.info("### END logging revocation of parent ID: " + parent_id)