„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
parent
7b07615720
commit
cd4447cdc4
|
|
@ -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}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue