From 7711b9fcdd2a45e0d36b4d67374fafcdabc2be36 Mon Sep 17 00:00:00 2001 From: Georg Klein Date: Wed, 10 Mar 2021 15:36:59 +0100 Subject: [PATCH] initial commit --- README.md | 0 importUsers.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 README.md create mode 100755 importUsers.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/importUsers.py b/importUsers.py new file mode 100755 index 0000000..d222c10 --- /dev/null +++ b/importUsers.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 + +# Simple Cloudron bulk user import script +# Uses the cloudron REST API v1 +# Rev 1.1, 03/07/21, changes: +# - added additional code and option -m to automatically create mailboxes on the appropriate domain. +# This should only be used if the mailaddress in the CSV-file belongs to the current cloudron (sub)domain +# CVS file requirements: +# - csv separator is ';' not ',' (which makes it a not-standard csv file, actually) +# - fieldnames [first_name, last_name, email_address, sis_username, person_id] +# - first three are self explanatory. +# Of these, only email_address is required by cloudron. the other fields must exist but can be empty +# - sis_username is turned into the optional username in cloudron, field must exist but can be empty +# - person_id follows a DATA-DATA-nn scheme, where only the nn part is used +# - example for a minimum CSV file: +# person_id;first_name;last_name;email_address;sis_username +# SOME-STUFF-01;Douglas;Adams;douglas.adams@example.com;d.adams +# ToDo: +# - group membership is hardcoded to our needs right now (s. FIX ME below), this is a Really Bad Idea (tm) +# - csv input format follows the AppleSchoolManager students.csv format, this should be more general +# see https://support.apple.com/guide/apple-school-manager/students-template-tes1bd5a3b1e/web for details +# - 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) + +import sys, getopt +import getpass +import requests +import csv +from json import loads + +def requestAccessToken(domain, username=''): + apiBasePath = 'https://' + domain + "/api/v1" + if( username == '' ): + username = input("Enter username for " + domain + ": ") + + password = getpass.getpass("Enter password for " + domain + ": ") + 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'] + else: + print("Error requesting access token: ", a.status_code) + sys.exit(2) + +def main(argv): + domain = '' + username = '' + dataFilePath = './users.csv' + + try: + opts, args = getopt.getopt(argv,"hf:d:u:m",["help","file=","domain=","username=","add-mailbox"]) + except getopt.GetoptError: + print("importUsers.py -f -d -u ") + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print("importUsers.py -f -d -u ") + sys.exit() + elif opt in ('-f', '--file'): + dataFilePath = arg + elif opt in ('-d', '--domain'): + domain = arg + elif opt in ('-u','--username'): + username = arg + elif opt in ('-m','--add-mailbox'): + addmailbox = True + + if( domain == '' ): + print("domainname must be provided, use the -d flag") + sys.exit(2) + + apiBasePath = 'https://' + domain + "/api/v1" + + # get current access Token + accessToken = requestAccessToken(domain, username) + + # read existing groups from the cloudron and build an abbreviated dictionary of name and id + r = requests.get(apiBasePath + '/groups?access_token='+accessToken) + groupData = loads(r.text)['groups'] + groups = {} + for g in groupData: + groups[g['name']] = g['id'] + + # read users from csv file and add to cloudron + with open(dataFilePath) as users: + reader = csv.DictReader(users, delimiter=';') + for entry in reader: + # fill payload object with data from the csv + 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":""} + + r = requests.post(requestUrl, json=payload) + if( r.status_code == requests.codes.created ): + print("User " + displayName + " succesfully created") + curUserId = r.json()['id'] + userUrl = apiBasePath + '/users/'+curUserId+'/groups?access_token='+accessToken + + # look up current user's group id and prepare the groups array for adding the user to the groups + # FIX ME: this section uses hardcoded information to calculate the appropriate group names for our naming scheme + classGroup = 'schueler-hd-' + entry['person_id'].split('-')[2].lower() + userGroups = {"groupIds":[groups['Schueler'],groups[classGroup]]} + p = requests.put(userUrl,userGroups) + if( p.status_code == requests.codes.no_content): + print("User " + displayName + " added to groups 'Schueler' & " + classGroup) + else: + print("Could not add user " + displayName + " to groups!") + + # If a mailbox was to be added, do that now (we have the id already) + if( addmailbox == True ): + payload = {"name":entry['sis_username'], "ownerId":curUserId, "ownerType":"user"} + userDomain = entry['email_address'].split('@')[-1] + mailUrl = apiBasePath + '/mail/'+userDomain+'/mailboxes?access_token='+accessToken + p = requests.post(mailUrl, json=payload) + if( p.status_code == requests.codes.created ): + print("Mailbox for user " + displayName + " created as " + entry['email_address']) + else: + print("Could not create mailbox for user " + displayName + ", error code " + p.status_code) + + else: + print('User ' + displayName + ' could not be created, statuscode ', r.status_code) + +if __name__ == "__main__": + main(sys.argv[1:]) \ No newline at end of file