From 7b076157206c6e9d8053e3cd0ce33f7b4ba05086 Mon Sep 17 00:00:00 2001 From: Georg Klein Date: Wed, 19 Oct 2022 21:13:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Dateien=20hochladen=20nach=20=E2=80=9E?= =?UTF-8?q?=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool generates a list suitable for importUsers from a simple comma-separated list of first and last names and some command line parameters Requires an additional file named WordList.txt containing a list of words for default password generation. Three words are picked at random and joined with a dash --- generateUserlist.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 generateUserlist.py diff --git a/generateUserlist.py b/generateUserlist.py new file mode 100644 index 0000000..98c5a19 --- /dev/null +++ b/generateUserlist.py @@ -0,0 +1,49 @@ +# Generate import file + +import sys, getopt +import csv +import os,io +import fileinput +import random + +def main(argv): + wordList = open("WordList.txt").read().splitlines() + groups = "Schueler" + yoe = "00" + domainname="hd.waldorf.one" + filename = "names.csv" + + try: + opts, args = getopt.getopt(argv,"hg:d:y:f:",["help","groups=","domain=","year=","file="]) + except getopt.GetoptError: + print("Usage: importUsers.py -d [-g ] [-y -f ") + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print("Usage: importUsers.py -d [-g ] -y -f ") + elif opt in ('-d', '--domain'): + domainname = arg + elif opt in ('-g', '--groups'): + groups = groups + "," + arg + elif opt in ('-f', '--file'): + filename = arg + elif opt in ('-y', '--year'): + yoe = arg + groups = groups + ",schueler-hd-" + yoe + + outputFileName = "users_" + yoe + ".csv" + char_map = {ord("ä"):"ae", ord("ü"):"ue", ord("ö"):"oe", ord("ß"):"ss", ord(" "):"-"} + with open(outputFileName,"w+",newline='') as outputFile: + header = ["first_name","last_name","email_address","sis_username","user_groups","password"] + writer = csv.DictWriter(outputFile,fieldnames=header,delimiter=";", quoting=csv.QUOTE_MINIMAL) + writer.writeheader() + users = open(filename,"r").read().splitlines() + for user in users: + first_name,last_name = tuple(user.split(",")) + username = first_name.casefold().translate(char_map) + "." + last_name.casefold().translate(char_map) + "-" + yoe + address = username + "@" + domainname + password = "-".join(random.sample(wordList,3)) + writer.writerow({"first_name":first_name,"last_name":last_name,"email_address":address,"sis_username":username,"user_groups":groups,"password":password}) + +if __name__ == "__main__": + main(sys.argv[1:]) \ No newline at end of file From cd4447cdc402810c04cbae20149dd5fe1c08c69e Mon Sep 17 00:00:00 2001 From: Georg Klein Date: Wed, 19 Oct 2022 21:15:15 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=80=9EimportUsers.py=E2=80=9C=20=C3=A4nd?= =?UTF-8?q?ern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added local storage of access token including a limited means to check for validity - if mailboxes were supposed to be added and that failed, the tool will dump the missing users into a csv file formatted to import into the Cloudron Email management section. --- importUsers.py | 58 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/importUsers.py b/importUsers.py index 93c2cb7..bcb01fb 100755 --- a/importUsers.py +++ b/importUsers.py @@ -1,6 +1,16 @@ # Simple Cloudron bulk user import script # Uses the Cloudron REST API v1 -# Rev 1.2, 03/019/21, changes: +# Rev 1.4, 10/19/22, changes: +# - added local storage of access token including a limited means to check for validity +# - if mailboxes were supposed to be added and that failed, the tool will dump the missing users +# into a csv file formatted to import into the Cloudron Email management section. +# Rev 1.3, 10/16/22, changes: +# - added option to add passwords as part of the import +# Please note that this method obviously discloses passwords to the admin, so +# users should be encouraged to change their password on first login. +# - updated help output to include the optional mailbox and password options +# - fixed the revision date of the last commit +# Rev 1.2, 03/19/21, changes: # - removed the she-bang because it was interfering with alternate python paths. Use python importUsers.py to run # - changed the CSV file format. The person_id field is gone, user_groups is in. This change allows flexibility # on group membership (arbitrary number of groups) @@ -21,9 +31,7 @@ # Douglas;Adams;douglas.adams@example.com;d.adams;staff,writers # ToDo: # - groups from the user_group field must already exist. If a specified group is not available, an error is displayed. -# Future revisions could ask whether missing groups should be created -# - currently, a new access token is generated for every run of the script. -# This could be stored somewhere for consecutive runs, the standard expiry is a year +# Future revisions could ask whether missing groups should be created. # - could use some sanity checks for input (low priority, if you want to sabotage yourself, go ahead) # - totpTokens are always requested, even if the user hasn't configured token 2FA (they probably should) # - Check, if the supplied email address is in the same domain as the Cloudron if the -m option is set @@ -32,9 +40,23 @@ import sys, getopt import getpass import requests import csv +import os +import pickle from json import loads +from datetime import datetime +from dateutil.relativedelta import relativedelta def requestAccessToken(domain, username=''): + # check for valid access token + if os.path.exists(".cat.pkl"): + with open(".cat.pkl","rb") as cloudronAccessTokenFile: + accessTokenDict = pickle.load(cloudronAccessTokenFile) + + terminationDate = datetime.now() + relativedelta(years=1) + if (accessTokenDict['domain'] == domain) and (accessTokenDict['date'] < terminationDate): + return accessTokenDict['token'] + + # current token is either invalid or for another domain, create a new one apiBasePath = 'https://' + domain + "/api/v1" if( username == '' ): username = input("Enter username for " + domain + ": ") @@ -43,7 +65,12 @@ def requestAccessToken(domain, username=''): totpToken = input("Enter current totpToken: ") a = requests.post(apiBasePath + '/cloudron/login', json={"username":username, "password":password, "totpToken":totpToken}) if( a.status_code == requests.codes.ok ): - return a.json()['accessToken'] + accessToken = a.json()['accessToken'] + accessTokenDict = {"domain":domain,"date":datetime.now(),"token":accessToken} + with open(".cat.pkl","wb+") as cloudronAccessTokenFile: + pickle.dump(accessTokenDict,cloudronAccessTokenFile) + + return accessToken else: print(f"Error requesting access token: {a.status_code}") sys.exit(2) @@ -53,24 +80,28 @@ def main(argv): username = '' dataFilePath = './users.csv' addmailbox = False + addpassword = False try: - opts, args = getopt.getopt(argv,"hf:d:u:m",["help","file=","domain=","username=","add-mailbox"]) + opts, args = getopt.getopt(argv,"hf:d:u:mp",["help","file=","domain=","username=","add-mailbox","add-password"]) except getopt.GetoptError: - print("importUsers.py -f -d -u ") + print("importUsers.py -f -d [-u ] [-m] [-p]") sys.exit(2) for opt, arg in opts: if opt == '-h': - print("importUsers.py -f -d -u ") + print("importUsers.py -f -d -u [-m] [-p]") sys.exit() elif opt in ('-f', '--file'): dataFilePath = arg + directory,dataFileName = os.path.split(dataFilePath) elif opt in ('-d', '--domain'): domain = arg elif opt in ('-u','--username'): username = arg elif opt in ('-m','--add-mailbox'): addmailbox = True + elif opt in ('-p','--add-password'): + addpassword = True if( domain == '' ): print("domainname must be provided, use the -d flag") @@ -96,7 +127,10 @@ def main(argv): displayName = entry['first_name'] + ' ' + entry['last_name'] requestUrl = apiBasePath + '/users?access_token='+accessToken - payload = {"email":entry['email_address'], "username":entry['sis_username'], "displayName":displayName, "password":""} + password = "" + if( addpassword == True ): + password = entry['password'] + payload = {"email":entry['email_address'], "username":entry['sis_username'], "displayName":displayName, "password":password} r = requests.post(requestUrl, json=payload) if( r.status_code == requests.codes.created ): @@ -123,6 +157,7 @@ def main(argv): # If a mailbox was to be added, do that now (we have the id already) if( addmailbox == True ): + missingMailboxesFilename = "mailbox_" + dataFileName payload = {"name":entry['sis_username'], "ownerId":curUserId, "ownerType":"user"} userDomain = entry['email_address'].split('@')[-1] mailUrl = apiBasePath + '/mail/'+userDomain+'/mailboxes?access_token='+accessToken @@ -131,6 +166,11 @@ def main(argv): print(f"Mailbox for user {displayName} created as {entry['email_address']}") else: print(f"Could not create mailbox for user {displayName}, error code {p.status_code}") + print(f"Dumping data to import file ") + + with open(missingMailboxesFilename,"a+",newline="") as mailboxFile: + writer = csv.writer(mailboxFile,delimiter=",") + writer.writerow([entry['sis_username'],userDomain,curUserId,"user"]) else: print(f"User {displayName} could not be created, statuscode {r.status_code}")