diff --git a/README.md b/README.md index 7a16064..af430e2 100644 --- a/README.md +++ b/README.md @@ -205,16 +205,20 @@ optional arguments: Zone of the user who creates user (only available in Yoda 1.9 and higher) The CSV file is expected to include the following labels in its header (the first row): - 'category' = category for the group - 'subcategory' = subcategory for the group - 'groupname' = name of the group (without the "research-" prefix) + 'category' = category for the group + 'subcategory' = subcategory for the group + 'groupname' = name of the group (without the "research-" prefix) + + For Yoda versions 1.9 and higher, these labels can optionally be included: + 'expiration_date' = expiration date for the group. + 'schema_id' = schema id for the group. Can only be set when the group is first created. The remainder of the columns should have a label that starts with a prefix which indicates the role of each group member: - 'manager:' = user that will be given the role of manager - 'member:' = user that will be given the role of member with read/write - 'viewer:' = user that will be given the role of viewer with read + 'manager:' = user that will be given the role of manager + 'member:' = user that will be given the role of member with read/write + 'viewer:' = user that will be given the role of viewer with read Notes: - Columns may appear in any order @@ -224,6 +228,11 @@ optional arguments: category,subcategory,groupname,manager:manager,member:member1,member:member2 departmentx,teama,groupteama,m.manager@example.com,m.member@example.com,n.member@example.com departmentx,teamb,groupteamb,m.manager@example.com,p.member@example.com, + + Example Yoda 1.9 and higher: + category,subcategory,groupname,manager:manager,member:member1,expiration_date,schema_id + departmentx,teama,groupteama,m.manager@example.com,m.member@example.com,2025-01-01,default-2 + departmentx,teamb,groupteamb,m.manager@example.com,p.member@example.com,, ``` ### yreport\_collectionsize diff --git a/yclienttools/common_rules.py b/yclienttools/common_rules.py index 92876e6..3dd653a 100644 --- a/yclienttools/common_rules.py +++ b/yclienttools/common_rules.py @@ -9,6 +9,7 @@ from yclienttools import common_queries from yclienttools.exceptions import SizeNotSupportedException + class RuleInterface: def __init__(self, session, yoda_version): @@ -22,12 +23,12 @@ def __init__(self, session, yoda_version): """ self.session = session self.set_re = False if yoda_version == "1.7" else True - self.uuGroupAdd_version = "1.7" if yoda_version in ["1.7", "1.8"] else "1.9" + self.uuGroupAdd_version = "1.7" if yoda_version in [ + "1.7", "1.8"] else "1.9" self.default_rule_engine = 'irods_rule_engine_plugin-irods_rule_language-instance' - def call_rule(self, rulename, params, number_outputs, - rule_engine = None): + rule_engine=None): """Run a rule :param rulename: name of the rule @@ -40,23 +41,25 @@ def call_rule(self, rulename, params, number_outputs, for input_var in params.keys(): body += "*{},".format(input_var) - outparams = list(map(lambda n : '*outparam{}'.format(str(n+1)), range(number_outputs))) + outparams = list( + map(lambda n: '*outparam{}'.format(str(n + 1)), range(number_outputs))) body += '{}); writeLine("stdout","{}")}}'.format( ",".join(outparams), "\n".join(outparams)) - input_params = { "*{}".format(k) : '"{}"'.format(v) for (k,v) in params.items() } + input_params = {"*{}".format(k): '"{}"'.format(v) + for (k, v) in params.items()} output_params = 'ruleExecOut' if self.set_re: - re_config = { 'instance_name': self.default_rule_engine if rule_engine is None - else rule_engine } + re_config = {'instance_name': self.default_rule_engine if rule_engine is None + else rule_engine} else: re_config = {} myrule = Rule( self.session, - rule_file = StringIO(body), + rule_file=StringIO(body), params=input_params, output=output_params, **re_config) @@ -67,145 +70,156 @@ def call_rule(self, rulename, params, number_outputs, return buf[:number_outputs] - def _string_list_to_list(self, s): if s.startswith("[") and s.endswith("]"): return s[1:-1].split(",") else: - raise ValueError("Unable to convert string representation of list to list") - + raise ValueError( + "Unable to convert string representation of list to list") def call_uuGroupGetMembers(self, groupname): - """Returns list of group members""" - parms = OrderedDict([ - ( 'groupname', groupname)] ) - [out] = self.call_rule('uuGroupGetMembers', parms, 1) - if len(out) >= 1023 and not out.endswith("]"): - raise SizeNotSupportedException("Group member list exceeds 1023 bytes") - return self._string_list_to_list(out) - + """Returns list of group members""" + parms = OrderedDict([ + ('groupname', groupname)]) + [out] = self.call_rule('uuGroupGetMembers', parms, 1) + if len(out) >= 1023 and not out.endswith("]"): + raise SizeNotSupportedException( + "Group member list exceeds 1023 bytes") + return self._string_list_to_list(out) def call_uuGroupUserRemove(self, groupname, user): - """Removes a user from a group""" - parms = OrderedDict([ - ( 'groupname', groupname), - ( 'user', user) ]) - return self.call_rule('uuGroupUserRemove', parms, 2) - + """Removes a user from a group""" + parms = OrderedDict([ + ('groupname', groupname), + ('user', user)]) + return self.call_rule('uuGroupUserRemove', parms, 2) def call_uuGroupGetMemberType(self, groupname, user): - """:returns: member type of a group member""" - parms = OrderedDict([ - ( 'groupname', groupname), - ( 'user', user) ]) - return self.call_rule('uuGroupGetMemberType', parms, 1)[0] - - def call_uuGroupUserAddByOtherCreator(self, groupname, username, creator_user, creator_zone): - """Adds user to group on the behalf of a creator user. - - :param: groupname - :param: username - :param: creator_user - :param: creator_zone - :returns: (status, message) ; status !=0 is error - """ - parms = OrderedDict([ - ('groupname', groupname), - ('username', username), - ('creatorUser', creator_user), - ('creatorZone', creator_zone)]) - return self.call_rule('uuGroupUserAdd', parms, 2) + """:returns: member type of a group member""" + parms = OrderedDict([ + ('groupname', groupname), + ('user', user)]) + return self.call_rule('uuGroupGetMemberType', parms, 1)[0] + + def call_uuGroupUserAddByOtherCreator( + self, groupname, username, creator_user, creator_zone): + """Adds user to group on the behalf of a creator user. + + :param: groupname + :param: username + :param: creator_user + :param: creator_zone + :returns: (status, message) ; status !=0 is error + """ + parms = OrderedDict([ + ('groupname', groupname), + ('username', username), + ('creatorUser', creator_user), + ('creatorZone', creator_zone)]) + return self.call_rule('uuGroupUserAdd', parms, 2) def call_uuGroupUserAdd(self, groupname, username): - """Adds user to group. - - :param: groupname - :param: username - :returns: (status, message) ; status !=0 is error - """ - parms = OrderedDict([ - ('groupname', groupname), - ('username', username)]) - return self.call_rule('uuGroupUserAdd', parms, 2) + """Adds user to group. + :param: groupname + :param: username + :returns: (status, message) ; status !=0 is error + """ + parms = OrderedDict([ + ('groupname', groupname), + ('username', username)]) + return self.call_rule('uuGroupUserAdd', parms, 2) def call_uuGroupUserChangeRole(self, groupname, username, newrole): - """Change role of user in group - - :param groupname: name of group - :param username: name of user - :param newrole: new role (can be "manager", "reader", "normal") - :returns: (status, message) ; status != 0 is error - """ - parms = OrderedDict([ - ('groupname', groupname), - ('username', username), - ('newrole', newrole)]) - return self.call_rule('uuGroupUserChangeRole', parms, 2) + """Change role of user in group + :param groupname: name of group + :param username: name of user + :param newrole: new role (can be "manager", "reader", "normal") + :returns: (status, message) ; status != 0 is error + """ + parms = OrderedDict([ + ('groupname', groupname), + ('username', username), + ('newrole', newrole)]) + return self.call_rule('uuGroupUserChangeRole', parms, 2) def call_uuGroupExists(self, groupname): - """Check whether group name exists on Yoda - - :param groupname: name of group - :returns: false/true - """ - parms = OrderedDict([('groupname', groupname)]) - [out] = self.call_rule('uuGroupExists', parms, 1) - return out == 'true' + """Check whether group name exists on Yoda + :param groupname: name of group + :returns: false/true + """ + parms = OrderedDict([('groupname', groupname)]) + [out] = self.call_rule('uuGroupExists', parms, 1) + return out == 'true' def call_uuUserExists(self, username): - """Check whether user name exists on Yoda - - :param username: name of user - :returns: false/true - """ - parms = OrderedDict([('username', username)]) - [out] = self.call_rule('uuUserExists', parms, 1) - return out == 'true' + """Check whether user name exists on Yoda + :param username: name of user + :returns: false/true + """ + parms = OrderedDict([('username', username)]) + [out] = self.call_rule('uuUserExists', parms, 1) + return out == 'true' def call_uuGroupAdd(self, groupname, category, - subcategory, description, classification): - """Adds a group - - :param groupname: name of group - :param category: category / community - :param subcategory: subcategory - :param description: description - :param classification: security classification - - :returns: (status, message). Status not 0 means error, - -1089000 means group name already exists - """ - if self.uuGroupAdd_version == "1.7": - parms = OrderedDict([ - ('groupname', groupname), - ('category', category), - ('subcategory', subcategory), - ('description', description), - ('classification', classification)]) - elif self.uuGroupAdd_version == "1.9": - parms = OrderedDict([ - ('groupname', groupname), - ('category', category), - ('subcategory', subcategory), - ('schema_id', 'default-2'), - ('expiration_date', ''), - ('description', description), - ('dataClassification', classification), - ('co_identifier', '') - ]) - - return self.call_rule('uuGroupAdd', parms, 2) + subcategory, description, classification, schema_id='default-2', expiration_date=''): + """Adds a group + + :param groupname: name of group + :param category: category / community + :param subcategory: subcategory + :param description: description + :param classification: security classification + :param schema_id: schema id + :param expiration_date: expiration date + + :returns: (status, message). Status not 0 means error, + -1089000 means group name already exists + """ + if self.uuGroupAdd_version == "1.7": + parms = OrderedDict([ + ('groupname', groupname), + ('category', category), + ('subcategory', subcategory), + ('description', description), + ('classification', classification)]) + elif self.uuGroupAdd_version == "1.9": + parms = OrderedDict([ + ('groupname', groupname), + ('category', category), + ('subcategory', subcategory), + ('schema_id', schema_id if schema_id not in ("", ".") else "default-2"), + ('expiration_date', expiration_date), + ('description', description), + ('dataClassification', classification), + ('co_identifier', '') + ]) + + return self.call_rule('uuGroupAdd', parms, 2) + + def call_uuGroupModify(self, groupname, property, value): + """Modifies one property of a group + + :param groupname: name of group + :param property: property to change + :param value: value to change the property to + + :returns: (status, message). Status not 0 means error. + """ + parms = OrderedDict([('groupname', groupname), + ('property', property), + ('value', value)]) + return self.call_rule('uuGroupModify', parms, 2) def call_uuGroupRemove(self, groupname): - """Removes an empty group + """Removes an empty group - :param groupname: name of group + :param groupname: name of group - :returns: (status, message). Status not 0 means error. - """ - parms = OrderedDict([('groupname', groupname)]) - return self.call_rule('uuGroupRemove', parms, 2) + :returns: (status, message). Status not 0 means error. + """ + parms = OrderedDict([('groupname', groupname)]) + return self.call_rule('uuGroupRemove', parms, 2) diff --git a/yclienttools/importgroups.py b/yclienttools/importgroups.py index dda3123..63ac39d 100644 --- a/yclienttools/importgroups.py +++ b/yclienttools/importgroups.py @@ -3,6 +3,7 @@ import csv import sys import re +from datetime import datetime import dns.resolver as resolver @@ -19,7 +20,8 @@ # Based on yoda-batch-add script by Ton Smeele -def parse_csv_file(input_file, args): + +def parse_csv_file(input_file, args, yoda_version): extracted_data = [] with open(input_file, mode="r", encoding="utf-8-sig") as csv_file: @@ -34,39 +36,53 @@ def parse_csv_file(input_file, args): restkey='OTHERDATA') row_number = 1 # header row already read - for label in _get_csv_predefined_labels(): + for label in _get_csv_required_labels(): if label not in reader.fieldnames: _exit_with_error( 'CSV header is missing compulsory field "{}"'.format(label)) - duplicate_columns = _get_duplicate_columns(reader.fieldnames) - if ( len(duplicate_columns) > 0 ): - _exit_with_error("File has duplicate column(s): " + str(duplicate_columns) ) + duplicate_columns = _get_duplicate_columns( + reader.fieldnames, yoda_version) + if (len(duplicate_columns) > 0): + _exit_with_error( + "File has duplicate column(s): " + str(duplicate_columns)) for line in reader: row_number += 1 - rowdata, error = _process_csv_line(line, args) + rowdata, error = _process_csv_line(line, args, yoda_version) if error is None: extracted_data.append(rowdata) else: - _exit_with_error("Data error in in row {}: {}".format( + _exit_with_error("Data error in row {}: {}".format( str(row_number), error)) return extracted_data -def _get_csv_predefined_labels(): +def _get_csv_required_labels(): return ['category', 'subcategory', 'groupname'] -def _get_duplicate_columns(fields_list): +def _get_csv_1_9_exclusive_labels(): + """Returns labels that can only appear with yoda version 1.9 and higher.""" + return ['expiration_date', 'schema_id'] + + +def _get_csv_predefined_labels(yoda_version): + if yoda_version in ('1.7', '1.8'): + return ['category', 'subcategory', 'groupname'] + else: + return ['category', 'subcategory', 'groupname', 'expiration_date', 'schema_id'] + + +def _get_duplicate_columns(fields_list, yoda_version): fields_seen = set() duplicate_fields = set() for field in fields_list: - if ( field in _get_csv_predefined_labels() or - field.startswith( ("manager:", "viewer:", "member:"))): + if (field in _get_csv_predefined_labels(yoda_version) or + field.startswith(("manager:", "viewer:", "member:"))): if field in fields_seen: duplicate_fields.add(field) else: @@ -75,10 +91,12 @@ def _get_duplicate_columns(fields_list): return duplicate_fields -def _process_csv_line(line, args): +def _process_csv_line(line, args, yoda_version): category = line['category'].strip().lower().replace('.', '') subcategory = line['subcategory'].strip() groupname = "research-" + line['groupname'].strip().lower() + schema_id = line['schema_id'] if 'schema_id' in line else '' + expiration_date = line['expiration_date'] if 'expiration_date' in line else '' managers = [] members = [] viewers = [] @@ -86,7 +104,9 @@ def _process_csv_line(line, args): for column_name in line.keys(): if column_name == '': return None, 'Column cannot have an empty label' - elif column_name in _get_csv_predefined_labels(): + elif yoda_version in ('1.7', '1.8') and column_name in _get_csv_1_9_exclusive_labels(): + return None, 'Column "{}" is only supported in Yoda 1.9 and higher'.format(column_name) + elif column_name in _get_csv_predefined_labels(yoda_version): continue username = line.get(column_name) @@ -129,15 +149,22 @@ def _process_csv_line(line, args): if not is_valid_groupname(groupname): return None, '"{}" is not a valid group name.'.format(groupname) - row_data = (category, subcategory, groupname, managers, members, viewers) + if not is_valid_schema_id(schema_id): + return None, '"{}" is not a valid schema id.'.format(schema_id) + + if not is_valid_expiration_date(expiration_date): + return None, '"{}" is not a valid expiration date.'.format(expiration_date) + + row_data = (category, subcategory, groupname, managers, + members, viewers, schema_id, expiration_date) return row_data, None -def _are_roles_equivalent(a,b): +def _are_roles_equivalent(a, b): """Checks whether two roles are equivalent. Needed because Yoda and Yoda-clienttools use slightly different names for the roles.""" - r_role_names = [ "viewer", "reader" ] - m_role_names = [ "member", "normal" ] + r_role_names = ["viewer", "reader"] + m_role_names = ["member", "normal"] if a == b: return True @@ -148,24 +175,29 @@ def _are_roles_equivalent(a,b): else: return False + def is_email(username): return re.search(r'@.*[^\.]+\.[^\.]+$', username) is not None + @lru_cache(maxsize=100) def is_valid_domain(domain): try: - return bool(resolver.query(domain, 'MX')) + return bool(resolver.query(domain, 'MX')) except (resolver.NXDOMAIN, resolver.NoAnswer): - return False + return False + def is_valid_category(name): """Is this name a valid (sub)category name?""" return re.search(r"^[a-zA-Z0-9\-_]+$", name) is not None + def is_valid_groupname(name): """Is this name a valid group name (prefix such as "research-" can be omitted""" return re.search(r"^[a-zA-Z0-9\-]+$", name) is not None + def is_internal_user(username, internal_domains): for domain in internal_domains: domain_pattern = '@{}$'.format(domain) @@ -175,9 +207,39 @@ def is_internal_user(username, internal_domains): return False +def is_valid_expiration_date(expiration_date): + """Validation of expiration date. + + :param expiration_date: String containing date that has to be validated + + :returns: Indication whether expiration date is an accepted value + """ + # Copied from rule_group_expiration_date_validate + if expiration_date in ["", "."]: + return True + + try: + if expiration_date != datetime.strptime(expiration_date, "%Y-%m-%d").strftime('%Y-%m-%d'): + raise ValueError + + # Expiration date should be in the future + if expiration_date <= datetime.now().strftime('%Y-%m-%d'): + raise ValueError + return True + except ValueError: + return False + + +def is_valid_schema_id(schema_id): + """Is this schema at least a correctly formatted schema-id?""" + if schema_id == "": + return True + return re.search(r"^[a-zA-Z0-9\-]+\-[0-9]+$", schema_id) is not None + + def validate_data(rule_interface, args, data): errors = [] - for (category, subcategory, groupname, managers, members, viewers) in data: + for (category, subcategory, groupname, managers, members, viewers, schema_id, expiration_date) in data: if rule_interface.call_uuGroupExists(groupname) and not args.allow_update: errors.append('Group "{}" already exists'.format(groupname)) @@ -186,7 +248,7 @@ def validate_data(rule_interface, args, data): # ensure that external users already have an iRODS account # we do not want to be the actor that creates them (unless # we are creating them in the name of a creator user) - if not rule_interface.call_uuUserExists(user)and not args.creator_user: + if not rule_interface.call_uuUserExists(user) and not args.creator_user: errors.append( 'Group {} has nonexisting external user {}'.format(groupname, user)) @@ -194,7 +256,7 @@ def validate_data(rule_interface, args, data): def apply_data(rule_interface, args, data): - for (category, subcategory, groupname, managers, members, viewers) in data: + for (category, subcategory, groupname, managers, members, viewers, schema_id, expiration_date) in data: new_group = False if args.verbose: @@ -203,14 +265,17 @@ def apply_data(rule_interface, args, data): # First create the group. Note that the rodsadmin actor will become a # groupmanager. [status, msg] = rule_interface.call_uuGroupAdd( - groupname, category, subcategory, '', 'unspecified') + groupname, category, subcategory, '', 'unspecified', schema_id, expiration_date) if ((status == '-1089000') | (status == '-809000')) and args.allow_update: print( 'WARNING: group "{}" not created, it already exists'.format(groupname)) + if schema_id != '': + print( + 'WARNING: group property "schema_id" not updated, as it can only be specified when the group is first created') elif status != '0': _exit_with_error( - "Error while attempting to create group {}. Status/message: {} / {}".format( + 'Error while attempting to create group "{}". Status/message: {} / {}'.format( groupname, status, msg)) @@ -220,26 +285,31 @@ def apply_data(rule_interface, args, data): # Now add the users and set their role if other than member allusers = managers + members + viewers for username in list(set(allusers)): # duplicates removed - currentrole = rule_interface.call_uuGroupGetMemberType(groupname, username) + currentrole = rule_interface.call_uuGroupGetMemberType( + groupname, username) if currentrole == "none": if args.creator_user: - [status, msg] = rule_interface.call_uuGroupUserAddByOtherCreator(groupname, username, args.creator_user, args.creator_zone) + [status, msg] = rule_interface.call_uuGroupUserAddByOtherCreator( + groupname, username, args.creator_user, args.creator_zone) else: - [status, msg] = rule_interface.call_uuGroupUserAdd(groupname, username) + [status, msg] = rule_interface.call_uuGroupUserAdd( + groupname, username) if status == '0': - currentrole = "member" - if args.verbose: - print("Notice: added user {} to group {}".format(username,groupname)) + currentrole = "member" + if args.verbose: + print("Notice: added user {} to group {}".format( + username, groupname)) else: print("Warning: error occurred while attempting to add user {} to group {}".format( - username, - groupname)) + username, + groupname)) print("Status: {} , Message: {}".format(status, msg)) else: if args.verbose: - print("Notice: user {} is already present in group {}.".format(username,groupname)) + print("Notice: user {} is already present in group {}.".format( + username, groupname)) # Set requested role. Note that user could be listed in multiple roles. # In case of multiple roles, manager takes precedence over normal, @@ -252,13 +322,15 @@ def apply_data(rule_interface, args, data): if _are_roles_equivalent(role, currentrole): if args.verbose: - print("Notice: user {} already has role {} in group {}.".format(username, role, groupname)) + print("Notice: user {} already has role {} in group {}.".format( + username, role, groupname)) else: [status, msg] = rule_interface.call_uuGroupUserChangeRole( groupname, username, role) if status == '0': if args.verbose: - print("Notice: changed role of user {} in group {} to {}".format(username, groupname, role)) + print("Notice: changed role of user {} in group {} to {}".format( + username, groupname, role)) else: print( "Warning: error while attempting to change role of user {} in group {} to {}".format( @@ -269,16 +341,30 @@ def apply_data(rule_interface, args, data): # Always remove the rods user for new groups, unless it is in the # CSV file. - if ( new_group and "rods" not in allusers and - rule_interface.call_uuGroupGetMemberType(groupname, "rods") != "none" ): - (status,msg) = rule_interface.call_uuGroupUserRemove(groupname, "rods") - if status == "0": - if args.verbose: - print("Notice: removed rods user from group " + groupname) - else: - if status !=0: - print ("Warning: error while attempting to remove user rods from group {}".format(groupname)) - print("Status: {} , Message: {}".format(status, msg)) + if (new_group and "rods" not in allusers and + rule_interface.call_uuGroupGetMemberType(groupname, "rods") != "none"): + (status, msg) = rule_interface.call_uuGroupUserRemove(groupname, "rods") + if status == "0": + if args.verbose: + print("Notice: removed rods user from group " + groupname) + else: + if status != 0: + print("Warning: error while attempting to remove user rods from group {}".format( + groupname)) + print("Status: {} , Message: {}".format(status, msg)) + + # Update expiration date if applicable + if not new_group and expiration_date not in ['', '.'] and args.allow_update: + [status, msg] = rule_interface.call_uuGroupModify( + groupname, "expiration_date", expiration_date) + if status == "0": + if args.verbose: + print("Notice: updated expiration date to {} for group {}".format( + expiration_date, groupname)) + else: + print("Warning: error while attempting to update expiration date to {} for group {}".format( + expiration_date, groupname)) + print("Status: {} , Message: {}".format(status, msg)) # Remove users not in sheet if args.delete: @@ -286,7 +372,8 @@ def apply_data(rule_interface, args, data): try: currentusers = rule_interface.call_uuGroupGetMembers(groupname) except SizeNotSupportedException: - print("Unable to check whether members of group {} need to be deleted.".format(groupname)) + print("Unable to check whether members of group {} need to be deleted.".format( + groupname)) print("Number of current members group is too large.") continue @@ -295,15 +382,18 @@ def apply_data(rule_interface, args, data): if user in managers: if len(managers) == 1: print("Error: cannot remove user {} from group {}, because he/she is the only group manager".format( - user,groupname)) + user, groupname)) continue else: managers.remove(user) if args.verbose: - print("Removing user {} from group {}".format(user,groupname)) - (status,msg) = rule_interface.call_uuGroupUserRemove(groupname, user) + print("Removing user {} from group {}".format( + user, groupname)) + (status, msg) = rule_interface.call_uuGroupUserRemove( + groupname, user) if status != "0": - print ("Warning: error while attempting to remove user {} from group {}".format(user,groupname)) + print("Warning: error while attempting to remove user {} from group {}".format( + user, groupname)) print("Status: {} , Message: {}".format(status, msg)) @@ -315,33 +405,38 @@ def print_parsed_data(data): print('No data loaded') else: for (category, subcategory, groupname, - managers, members, viewers) in data: + managers, members, viewers, schema_id, expiration_date) in data: print("Category: {}".format(category)) print("Subcategory: {}".format(subcategory)) print("Group: {}".format(groupname)) print("Managers: {}".format(','.join(managers))) print("Members: {}".format(','.join(members))) print("Readonly members: {}".format(','.join(viewers))) + print("Schema Id: {}".format(schema_id)) + print("Expiration Date: {}".format(expiration_date)) print() def entry(): '''Entry point''' args = _get_args() - data = parse_csv_file(args.csvfile, args) - yoda_version = args.yoda_version if args.yoda_version is not None else common_config.get_default_yoda_version() + yoda_version = args.yoda_version if args.yoda_version is not None else common_config.get_default_yoda_version() + data = parse_csv_file(args.csvfile, args, yoda_version) if args.offline_check or args.verbose: print_parsed_data(data) if args.delete and not args.allow_update: - _exit_with_error("Using the --delete option without the --allow-update option is not supported.") + _exit_with_error( + "Using the --delete option without the --allow-update option is not supported.") if (args.creator_user and not args.creator_zone) or (not args.creator_user and args.creator_zone): - _exit_with_error("Using the --creator-user option without the --creator-zone option is not supported.") + _exit_with_error( + "Using the --creator-user option without the --creator-zone option is not supported.") - if (args.creator_user and (yoda_version in ('1.7','1.8'))): - _exit_with_error("The --creator-user and --creator-zone options are only supported with Yoda versions 1.9 and higher.") + if (args.creator_user and (yoda_version in ('1.7', '1.8'))): + _exit_with_error( + "The --creator-user and --creator-zone options are only supported with Yoda versions 1.9 and higher.") if args.offline_check: sys.exit(0) @@ -399,16 +494,20 @@ def _get_args(): def _get_format_help_text(): return ''' The CSV file is expected to include the following labels in its header (the first row): - 'category' = category for the group - 'subcategory' = subcategory for the group - 'groupname' = name of the group (without the "research-" prefix) + 'category' = category for the group + 'subcategory' = subcategory for the group + 'groupname' = name of the group (without the "research-" prefix) + + For Yoda versions 1.9 and higher, these labels can optionally be included: + 'expiration_date' = expiration date for the group. Can only be set when the group is first created. + 'schema_id' = schema id for the group. Can only be set when the group is first created. The remainder of the columns should have a label that starts with a prefix which indicates the role of each group member: - 'manager:' = user that will be given the role of manager - 'member:' = user that will be given the role of member with read/write - 'viewer:' = user that will be given the role of viewer with read + 'manager:' = user that will be given the role of manager + 'member:' = user that will be given the role of member with read/write + 'viewer:' = user that will be given the role of viewer with read Notes: - Columns may appear in any order @@ -418,6 +517,11 @@ def _get_format_help_text(): category,subcategory,groupname,manager:manager,member:member1,member:member2 departmentx,teama,groupteama,m.manager@example.com,m.member@example.com,n.member@example.com departmentx,teamb,groupteamb,m.manager@example.com,p.member@example.com, + + Example Yoda 1.9 and higher: + category,subcategory,groupname,manager:manager,member:member1,expiration_date,schema_id + departmentx,teama,groupteama,m.manager@example.com,m.member@example.com,2025-01-01,default-2 + departmentx,teamb,groupteamb,m.manager@example.com,p.member@example.com,, '''