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}")