Generalizations

Comments, changes to reflect common usage scenarios
main
klongeiger 2022-10-22 16:08:57 +02:00
parent b71ae26a44
commit 3f0e8e0a24
3 changed files with 251 additions and 21 deletions

178
WordList.txt Normal file
View File

@ -0,0 +1,178 @@
Abend
Abendrot
Aldebaran
Allianz
Analyse
Angebot
Antwort
Aufgabe
Auftakt
Augenstern
Augenweide
Ausgleich
Bahnhof
Basis
Bedeutung
Begegnung
Beispiel
Beitrag
Beratung
Berufung
Bewegung
Blickfang
Butterblume
Borgelicht
Chance
Charakter
Chiffon
Denken
Dialog
Dutzend
Ebene
Einheit
Einigung
Energie
Farbe
Farbenfroh
Fernweh
Feuervogel
Firlefanz
Firmament
Forschung
Freiheit
Freude
Frieden
Fusion
Galerie
Garten
Gedanke
Gedanken
Gegenwart
Geist
Gestalt
Haltung
Heimat
Heimelig
Herbst
Herzlich
Himmel
Himmelsblau
Hoffnung
Insel
Interesse
Jahrzehnt
Jenseits
Joghurt
Jubel
Kamera
Karte
Kenntnis
Kleinod
Konzert
Kraft
Kultur
Kunst
Labsal
Lebenslust
Lehre
Libelle
Licht
Lichtblick
Lichtspiel
Liebe
Literatur
Lobhudeln
Luftikus
Luftschlange
Luftschloss
Medium
Meister
Mondschein
Morgenstern
Morgentau
Mummenschanz
Museum
Nachbar
Nacht
Naschkatze
Naseweis
Natur
Nordstern
Ofenwarm
Ohrenschmaus
Ohrwurm
Ordnung
Ostwind
Pflege
Plauderei
Prinzip
Purzelbaum
Pusteblume
Pustekuchen
Quasar
Quelle
Quitte
Rebell
Reform
Regenbogen
Richtung
Runde
Sammlung
Samtpfote
Sandkasten
Sandstein
Saumselig
Schatten
Schlummern
Schmollmund
Schutz
Seele
Sektlaune
Sommer
Sommerfrische
Sonne
Sperenzchen
Spiegel
Spielen
Sprache
Steckenpferd
Sternenzelt
Sternschnuppe
Sternstunde
Stimmung
Stubentiger
Szene
Tagtraum
Teilhabe
Tendenz
Theater
Tollpatsch
Tradition
Traum
Ukulele
Umwelt
Unsinn
Variante
Vertrauen
Vollmond
Vorfreude
Vorschlag
Waffel
Wahrheit
Wanderlust
Wildwasser
Wintertag
Wissen
Wundervoll
Wunschtraum
Xenon
Xenophil
Xylophon
Yacht
Yttrium
Yucca
Zeichen
Zeitlos
Zeitpunkt
Zeitraum
Zwielicht

View File

@ -1,38 +1,62 @@
# Generate import file # Generate Userlist
#
# Rev 1.1, 10/22/22
# - changed the -y (year of enrolement) parameter to a more general -s (suffix)
# - updated the usage message to include that change
# - added a lot of comments, especially regarding the default values
# - removed the unused fileinput import
# - changed the defaults to fit more common scenarios
# Rev 1.0, 10/20/22
# - a simple script to generate CSV-files suitable as input
# for the importUsers script.
# - this script is rather specifically designed to accomodate
# the needs of our school's user naming scheme. Different
# naming schemes should be easy to implement though.
# - the script generates default passwords by picking three
# words from a file called WordList.txt living in the same
# directory as the script file.
# Options
# -d,--domain: domain name for the email address, this is required
# -g,--groups: comma-separated list of groups added to every user
# -s,--suffix: added to every username and the resulting filename
# in this release, the suffix is added to the last group name as well
# -f,--file: file of usernames, first and last name separated by comma
import sys, getopt import sys, getopt
import csv import csv
import os,io import os,io
import fileinput
import random import random
def main(argv): def main(argv):
wordList = open("WordList.txt").read().splitlines() wordList = open("WordList.txt").read().splitlines()
groups = "Schueler" # add some default values for the options
yoe = "00" groups = "Users" # add default groups for all imports here
domainname="hd.waldorf.one" suffix = ""
domainname="example.com"
filename = "names.csv" filename = "names.csv"
# next is a list of characters common in users actual names but unsuitable for login names
# for this script, those are specific to german names. Spaces get replaced with dashes.
char_map = {ord("ä"):"ae", ord("ü"):"ue", ord("ö"):"oe", ord("ß"):"ss", ord(" "):"-"}
try: try:
opts, args = getopt.getopt(argv,"hg:d:y:f:",["help","groups=","domain=","year=","file="]) opts, args = getopt.getopt(argv,"hg:d:s:f:",["help","groups=","domain=","suffix=","file="])
except getopt.GetoptError: except getopt.GetoptError:
print("Usage: importUsers.py -d <domainname> [-g <groups>] [-y <year of enrollment]> -f <datafile>") print("Usage: importUsers.py -d <domainname> [-g <groups>] [-s <common suffix for this import]> -f <datafile>")
sys.exit(2) sys.exit(2)
for opt, arg in opts: for opt, arg in opts:
if opt == '-h': if opt == '-h':
print("Usage: importUsers.py -d <domainname> [-g <groups>] -y <year of enrollment> -f <datafile> ") print("Usage: importUsers.py -d <domainname> [-g <groups>] -s <common suffix for this import> -f <datafile> ")
elif opt in ('-d', '--domain'): elif opt in ('-d', '--domain'):
domainname = arg domainname = arg
elif opt in ('-g', '--groups'): elif opt in ('-g', '--groups'):
groups = groups + "," + arg groups = groups + "," + arg # as per importUsers specs, the groups are comma-separated
elif opt in ('-f', '--file'): elif opt in ('-f', '--file'):
filename = arg filename = arg
elif opt in ('-y', '--year'): elif opt in ('-s', '--suffix'):
yoe = arg suffix = arg
groups = groups + ",schueler-hd-" + yoe groups = groups + suffix
outputFileName = "users_" + yoe + ".csv" outputFileName = "users" + suffix + ".csv"
char_map = {ord("ä"):"ae", ord("ü"):"ue", ord("ö"):"oe", ord("ß"):"ss", ord(" "):"-"}
with open(outputFileName,"w+",newline='') as outputFile: with open(outputFileName,"w+",newline='') as outputFile:
header = ["first_name","last_name","email_address","sis_username","user_groups","password"] header = ["first_name","last_name","email_address","sis_username","user_groups","password"]
writer = csv.DictWriter(outputFile,fieldnames=header,delimiter=";", quoting=csv.QUOTE_MINIMAL) writer = csv.DictWriter(outputFile,fieldnames=header,delimiter=";", quoting=csv.QUOTE_MINIMAL)
@ -40,9 +64,9 @@ def main(argv):
users = open(filename,"r").read().splitlines() users = open(filename,"r").read().splitlines()
for user in users: for user in users:
first_name,last_name = tuple(user.split(",")) first_name,last_name = tuple(user.split(","))
username = first_name.casefold().translate(char_map) + "." + last_name.casefold().translate(char_map) + "-" + yoe username = first_name.casefold().translate(char_map) + "." + last_name.casefold().translate(char_map) + suffix
address = username + "@" + domainname address = username + "@" + domainname
password = "-".join(random.sample(wordList,3)) password = "-".join(random.sample(wordList,3)) # pick three words at ramdom, join them with a dash
writer.writerow({"first_name":first_name,"last_name":last_name,"email_address":address,"sis_username":username,"user_groups":groups,"password":password}) writer.writerow({"first_name":first_name,"last_name":last_name,"email_address":address,"sis_username":username,"user_groups":groups,"password":password})
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,5 +1,9 @@
# 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.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: # Rev 1.3, 10/16/22, changes:
# - added option to add passwords as part of the import # - added option to add passwords as part of the import
# Please note that this method obviously discloses passwords to the admin, so # Please note that this method obviously discloses passwords to the admin, so
@ -27,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
@ -38,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 + ": ")
@ -49,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)
@ -72,6 +93,7 @@ def main(argv):
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'):
@ -135,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
@ -143,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}")