diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index 1c441271c0ccd0fc20171ed11d4bb7cf64faa7da..229b95a62fbb218285c7d02a2410d11dd62b6387 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -3,3 +3,7 @@ LDAP_BASE_GROUPS="ou=groups,dc=example,dc=com"
 LDAP_SERVICE_BIND_DN=""
 LDAP_SERVICE_BIND_PASSWORD=""
 LDAP_SERVICE_URL="ldapi:///"
+LDAP_USER_OBJECTCLASSES=["vmailUser", "top", "inetOrgPerson", "organizationalPerson", "person", "posixAccount"]
+LDAP_USER_GID=20001
+LDAP_USER_MIN_UID=10000
+LDAP_USER_MAX_UID=18999
diff --git a/uffd/group/models.py b/uffd/group/models.py
index 23843df3bbe94fc44785c3bd29a96cdccfe77d73..98b60453878aab5e494f898e029d9779e995350d 100644
--- a/uffd/group/models.py
+++ b/uffd/group/models.py
@@ -26,7 +26,6 @@ class Group():
 	@classmethod
 	def from_ldap_dn(cls, dn):
 		conn = ldap.service_conn()
-		print(dn)
 		conn.search(dn, '(objectClass=groupOfUniqueNames)')
 		if not len(conn.entries) == 1:
 			return None
diff --git a/uffd/ldap/__init__.py b/uffd/ldap/__init__.py
index e1f09d811d8260c7bb2a43f5219aef9bbfa43746..61ffb9e0dde2158e82c80db24f77de409468ad95 100644
--- a/uffd/ldap/__init__.py
+++ b/uffd/ldap/__init__.py
@@ -1,4 +1,4 @@
 from .ldap import bp as ldap_bp
-from .ldap import service_conn, user_conn, escape_filter_chars
+from .ldap import service_conn, user_conn, escape_filter_chars, uid_to_dn, loginname_to_dn, get_next_uid
 
 bp = [ldap_bp]
diff --git a/uffd/ldap/ldap.py b/uffd/ldap/ldap.py
index b5d8b68a06b6cc2344a265e4bd11517ce2a41bf8..a9068cc7c1d913b8584cd795c2a4bfdda9887cfc 100644
--- a/uffd/ldap/ldap.py
+++ b/uffd/ldap/ldap.py
@@ -20,3 +20,31 @@ def service_conn():
 
 def user_conn():
 	pass
+
+def uid_to_dn(uid):
+	conn = service_conn()
+	conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format(escape_filter_chars(uid)))
+	if not len(conn.entries) == 1:
+		return None
+	else:
+		return conn.entries[0].entry_dn
+
+def loginname_to_dn(loginname):
+	return 'uid={},{}'.format(escape_filter_chars(loginname), current_app.config["LDAP_BASE_USER"])
+
+def get_next_uid():
+	conn = service_conn()
+	conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)')
+	max_uid = current_app.config["LDAP_USER_MIN_UID"]
+	for i in conn.entries:
+		# skip out of range entries
+		if i['uidNumber'].value > current_app.config["LDAP_USER_MAX_UID"]:
+			continue
+		if i['uidNumber'].value < current_app.config["LDAP_USER_MIN_UID"]:
+			continue
+		max_uid = max(i['uidNumber'].value, max_uid)
+	next_uid = max_uid + 1
+	if uid_to_dn(next_uid):
+		raise Exception('No free uid found')
+	else:
+		return next_uid
diff --git a/uffd/user/models.py b/uffd/user/models.py
index 734d07c23896226d7080fd7c736205e6fc9de646..a7ca635803d6b838764d3892a8b60aa664be5f29 100644
--- a/uffd/user/models.py
+++ b/uffd/user/models.py
@@ -1,4 +1,8 @@
 import string
+
+from ldap3 import MODIFY_REPLACE, HASHED_SALTED_SHA512
+from flask import current_app
+
 from uffd import ldap
 
 class User():
@@ -6,6 +10,7 @@ class User():
 	loginname = None
 	displayname = None
 	mail = None
+	newpassword = None
 
 	def __init__(self, uid=None, loginname='', displayname='', mail='', groups=None):
 		self.uid = uid
@@ -14,7 +19,7 @@ class User():
 		self.mail = mail
 		if isinstance(groups, str):
 			groups = [groups]
-		self.groups_ldap = groups
+		self.groups_ldap = groups or []
 		self._groups = None
 
 	@classmethod
@@ -36,7 +41,35 @@ class User():
 		return User.from_ldap(conn.entries[0])
 
 	def to_ldap(self, new):
-		pass
+		conn = ldap.service_conn()
+		if new:
+			attributes= {
+				'uidNumber': ldap.get_next_uid(),
+				'gidNumber': current_app.config['LDAP_USER_GID'],
+				'homeDirectory': '/home/'+self.loginname,
+				'sn': ' ',
+				# same as for update
+				'givenName': self.displayname,
+				'displayName': self.displayname,
+				'cn': self.displayname,
+				'mail': self.mail,
+			}
+			dn = ldap.loginname_to_dn(self.loginname)
+			result = conn.add(dn, current_app.config['LDAP_USER_OBJECTCLASSES'], attributes)
+		else:
+			attributes = {
+				'givenName': [(MODIFY_REPLACE, [self.displayname])],
+				'displayName': [(MODIFY_REPLACE, [self.displayname])],
+				'cn': [(MODIFY_REPLACE, [self.displayname])],
+				'mail': [(MODIFY_REPLACE, [self.mail])],
+				}
+			dn = ldap.uid_to_dn(self.uid)
+			result = conn.modify(dn, attributes)
+		if result:
+			if self.newpassword:
+				print(self.newpassword)
+				conn.extend.standard.modify_password(user=dn, old_password=None, new_password=self.newpassword, hash_algorithm=HASHED_SALTED_SHA512)
+		return result
 
 	def get_groups(self):
 		from uffd.group.models import Group
@@ -66,4 +99,4 @@ class User():
 		return True
 
 	def set_password(self, value):
-		raise Exception('TODO: user want to change passwords')
+		self.newpassword = value
diff --git a/uffd/user/templates/user.html b/uffd/user/templates/user.html
index 4818822bf100b4064b330f748b2b3eb3144ac037..e25d37baccf2bc8c1cc494a277b9254e3102abdd 100644
--- a/uffd/user/templates/user.html
+++ b/uffd/user/templates/user.html
@@ -1,7 +1,7 @@
 {% extends 'base.html' %}
 
 {% block body %}
-<form action="{{ url_for(".user_update", id_=user.uid) }}" method="POST">
+<form action="{{ url_for(".user_update", uid=user.uid) }}" method="POST">
 <div class="align-self-center">
 	<div class="form-group col">
 		<label for="user-uid">uid</label>
diff --git a/uffd/user/templates/user_list.html b/uffd/user/templates/user_list.html
index 54a9326ceeca4cd1e000a8090520d13314063eec..6635b97a88292e4cb9940e2c885fda8fcb30f581 100644
--- a/uffd/user/templates/user_list.html
+++ b/uffd/user/templates/user_list.html
@@ -8,7 +8,7 @@
 				<tr>
 					<th scope="col">uid</th>
 					<th scope="col">login name</th>
-					<th scope="col">given name</th>
+					<th scope="col">display name</th>
 					<th scope="col">
 						<a type="button" class="btn btn-primary" href="{{ url_for(".user_show") }}">
 							<i class="fa fa-plus" aria-hidden="true"></i> New
@@ -28,7 +28,7 @@
 						</a>
 					</td>
 					<td>
-						{{ user.givenname }}
+						{{ user.displayname }}
 					</td>
 					<td>
 						<a href="{{ url_for(".user_show", uid=user.uid) }}" class="btn btn-primary">
diff --git a/uffd/user/views.py b/uffd/user/views.py
index 80115181bd41994f3c7d7466a035043c1152d8dc..b6633bbb83cc0ea1dd03894547c14d1a40aeb352 100644
--- a/uffd/user/views.py
+++ b/uffd/user/views.py
@@ -23,16 +23,18 @@ def user_list():
 def user_show(uid=None):
 	if not uid:
 		user = User()
+		ldif = '<none yet>'
 	else:
 		conn = service_conn()
 		conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
 		assert len(conn.entries) == 1
 		user = User.from_ldap(conn.entries[0])
-	return render_template('user.html', user=user, user_ldif=conn.entries[0].entry_to_ldif())
+		ldif = conn.entries[0].entry_to_ldif()
+	return render_template('user.html', user=user, user_ldif=ldif)
 
 @bp.route("/<int:uid>/update", methods=['POST'])
 @bp.route("/new", methods=['POST'])
-def user_update(uid=None):
+def user_update(uid=False):
 	conn = service_conn()
 	if uid:
 		conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
@@ -51,7 +53,7 @@ def user_update(uid=None):
 	new_password = request.form.get('password')
 	if new_password:
 		user.set_password(new_password)
-	if user.to_ldap(conn, new=bool(uid)):
+	if user.to_ldap(new=(not uid)):
 		flash('User updated')
 	else:
 		flash('Error updating user: {}'.format(conn.result['message']))