„importUsers.py“ ändern

- 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.
main
Georg Klein 2022-10-19 21:15:15 +00:00
parent 7b07615720
commit cd4447cdc4
1 changed files with 49 additions and 9 deletions

View File

@ -1,6 +1,16 @@
# Simple Cloudron bulk user import script # Simple Cloudron bulk user import script
# Uses the Cloudron REST API v1 # 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 # - 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 # - 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) # on group membership (arbitrary number of groups)
@ -21,9 +31,7 @@
# Douglas;Adams;douglas.adams@example.com;d.adams;staff,writers # Douglas;Adams;douglas.adams@example.com;d.adams;staff,writers
# ToDo: # ToDo:
# - groups from the user_group field must already exist. If a specified group is not available, an error is displayed. # - 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 # 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
# - could use some sanity checks for input (low priority, if you want to sabotage yourself, go ahead) # - 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) # - 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 # - 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 getpass
import requests import requests
import csv import csv
import os
import pickle
from json import loads from json import loads
from datetime import datetime
from dateutil.relativedelta import relativedelta
def requestAccessToken(domain, username=''): 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" apiBasePath = 'https://' + domain + "/api/v1"
if( username == '' ): if( username == '' ):
username = input("Enter username for " + domain + ": ") username = input("Enter username for " + domain + ": ")
@ -43,7 +65,12 @@ def requestAccessToken(domain, username=''):
totpToken = input("Enter current totpToken: ") totpToken = input("Enter current totpToken: ")
a = requests.post(apiBasePath + '/cloudron/login', json={"username":username, "password":password, "totpToken":totpToken}) a = requests.post(apiBasePath + '/cloudron/login', json={"username":username, "password":password, "totpToken":totpToken})
if( a.status_code == requests.codes.ok ): 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: else:
print(f"Error requesting access token: {a.status_code}") print(f"Error requesting access token: {a.status_code}")
sys.exit(2) sys.exit(2)
@ -53,24 +80,28 @@ def main(argv):
username = '' username = ''
dataFilePath = './users.csv' dataFilePath = './users.csv'
addmailbox = False addmailbox = False
addpassword = False
try: 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: except getopt.GetoptError:
print("importUsers.py -f <datafile> -d <domainname> -u <username>") print("importUsers.py -f <datafile> -d <domainname> [-u <username>] [-m] [-p]")
sys.exit(2) sys.exit(2)
for opt, arg in opts: for opt, arg in opts:
if opt == '-h': if opt == '-h':
print("importUsers.py -f <datafile> -d <domainname> -u <username>") print("importUsers.py -f <datafile> -d <domainname> -u <username> [-m] [-p]")
sys.exit() sys.exit()
elif opt in ('-f', '--file'): elif opt in ('-f', '--file'):
dataFilePath = arg dataFilePath = arg
directory,dataFileName = os.path.split(dataFilePath)
elif opt in ('-d', '--domain'): elif opt in ('-d', '--domain'):
domain = arg domain = arg
elif opt in ('-u','--username'): elif opt in ('-u','--username'):
username = arg username = arg
elif opt in ('-m','--add-mailbox'): elif opt in ('-m','--add-mailbox'):
addmailbox = True addmailbox = True
elif opt in ('-p','--add-password'):
addpassword = True
if( domain == '' ): if( domain == '' ):
print("domainname must be provided, use the -d flag") print("domainname must be provided, use the -d flag")
@ -96,7 +127,10 @@ def main(argv):
displayName = entry['first_name'] + ' ' + entry['last_name'] displayName = entry['first_name'] + ' ' + entry['last_name']
requestUrl = apiBasePath + '/users?access_token='+accessToken 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) r = requests.post(requestUrl, json=payload)
if( r.status_code == requests.codes.created ): 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 a mailbox was to be added, do that now (we have the id already)
if( addmailbox == True ): if( addmailbox == True ):
missingMailboxesFilename = "mailbox_" + dataFileName
payload = {"name":entry['sis_username'], "ownerId":curUserId, "ownerType":"user"} payload = {"name":entry['sis_username'], "ownerId":curUserId, "ownerType":"user"}
userDomain = entry['email_address'].split('@')[-1] userDomain = entry['email_address'].split('@')[-1]
mailUrl = apiBasePath + '/mail/'+userDomain+'/mailboxes?access_token='+accessToken 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']}") print(f"Mailbox for user {displayName} created as {entry['email_address']}")
else: else:
print(f"Could not create mailbox for user {displayName}, error code {p.status_code}") print(f"Could not create mailbox for user {displayName}, error code {p.status_code}")
print(f"Dumping data to import file <mailbox_{dataFilePath}>")
with open(missingMailboxesFilename,"a+",newline="") as mailboxFile:
writer = csv.writer(mailboxFile,delimiter=",")
writer.writerow([entry['sis_username'],userDomain,curUserId,"user"])
else: else:
print(f"User {displayName} could not be created, statuscode {r.status_code}") print(f"User {displayName} could not be created, statuscode {r.status_code}")