From 4a9c455f1fc1a5b17f7d9f75275e3203ed58146e Mon Sep 17 00:00:00 2001 From: Julian Rother <julian@cccv.de> Date: Sat, 2 Oct 2021 19:58:59 +0200 Subject: [PATCH] Move User, Group and Mail models from LDAP to DB * Removal of ldapalchemy and LDAP mocking support * Removal of dependency on ldap3 (except for the migration) * Remaining "LDAP_<name>" config keys are renamed to "<name>" * Web interface to create, edit and delete groups * Consistent foreign key, cascading and nullable configuration on all models * User/Group.dn is replaced with numeric User/Group.id * User.uid is renamed to User.unix_uid (to differentiate with new User.id) * Group.gid is renamed to Group.unix_gid (to differentiate with new Group.id) * All User/Group/Mail related routes now use the database ids instead of uid/gid/dn * PasswordToken/MailToken now reference users directly instead of storing loginnames The database migration (optionally) uses the v1 config keys to connect to an LDAP server and to import all users, groups and mail forwardings. --- .pylintrc | 1 + README.md | 53 +- check_migrations.py | 24 +- debian/control | 4 +- setup.py | 3 +- .../ldap_server_entries_add.ldif | 42 - .../ldap_server_entries_cleanup.ldif | 17 - .../ldap_server_entries_modify.ldif | 17 - tests/openldap_mock/ldap_server_entries.json | 349 ------- tests/openldap_mock/ldap_server_info.json | 61 -- tests/openldap_mock/ldap_server_schema.json | 519 ---------- tests/test_invite.py | 158 ++- tests/test_mail.py | 31 +- tests/test_mfa.py | 79 +- tests/test_oauth2.py | 6 +- tests/test_role.py | 101 +- tests/test_rolemod.py | 19 +- tests/test_selfservice.py | 93 +- tests/test_services.py | 2 +- tests/test_session.py | 22 +- tests/test_signup.py | 38 +- tests/test_user.py | 221 ++-- tests/utils.py | 85 +- uffd/__init__.py | 40 +- uffd/api/views.py | 14 +- uffd/default_config.cfg | 58 +- uffd/invite/models.py | 30 +- uffd/invite/templates/invite/list.html | 8 +- uffd/invite/views.py | 14 +- uffd/lazyconfig.py | 54 - uffd/ldap.py | 145 --- uffd/ldapalchemy/__init__.py | 30 - uffd/ldapalchemy/attribute.py | 66 -- uffd/ldapalchemy/core.py | 284 ------ uffd/ldapalchemy/dbutils.py | 145 --- uffd/ldapalchemy/model.py | 148 --- uffd/ldapalchemy/relationship.py | 136 --- uffd/mail/models.py | 45 +- uffd/mail/views.py | 10 +- uffd/mfa/models.py | 18 +- uffd/mfa/views.py | 48 +- .../versions/878b25c4fae7_ldap_to_db.py | 956 ++++++++++++++++++ uffd/oauth2/models.py | 37 +- uffd/oauth2/views.py | 12 +- uffd/role/models.py | 52 +- uffd/role/templates/role/show.html | 16 +- uffd/role/views.py | 33 +- uffd/rolemod/templates/rolemod/show.html | 2 +- uffd/rolemod/views.py | 19 +- uffd/selfservice/models.py | 11 +- uffd/selfservice/views.py | 62 +- uffd/session/models.py | 9 +- uffd/session/views.py | 46 +- uffd/signup/models.py | 15 +- uffd/signup/views.py | 4 +- uffd/translations/de/LC_MESSAGES/messages.mo | Bin 31578 -> 31872 bytes uffd/translations/de/LC_MESSAGES/messages.po | 380 +++---- uffd/user/models.py | 163 ++- uffd/user/templates/group/list.html | 13 +- uffd/user/templates/group/show.html | 33 +- uffd/user/templates/user/list.html | 8 +- uffd/user/templates/user/show.html | 24 +- uffd/user/views_group.py | 52 +- uffd/user/views_user.py | 50 +- 64 files changed, 2072 insertions(+), 3163 deletions(-) delete mode 100644 tests/openldap_ldifs/ldap_server_entries_add.ldif delete mode 100644 tests/openldap_ldifs/ldap_server_entries_cleanup.ldif delete mode 100644 tests/openldap_ldifs/ldap_server_entries_modify.ldif delete mode 100644 tests/openldap_mock/ldap_server_entries.json delete mode 100644 tests/openldap_mock/ldap_server_info.json delete mode 100644 tests/openldap_mock/ldap_server_schema.json delete mode 100644 uffd/lazyconfig.py delete mode 100644 uffd/ldap.py delete mode 100644 uffd/ldapalchemy/__init__.py delete mode 100644 uffd/ldapalchemy/attribute.py delete mode 100644 uffd/ldapalchemy/core.py delete mode 100644 uffd/ldapalchemy/dbutils.py delete mode 100644 uffd/ldapalchemy/model.py delete mode 100644 uffd/ldapalchemy/relationship.py create mode 100644 uffd/migrations/versions/878b25c4fae7_ldap_to_db.py diff --git a/.pylintrc b/.pylintrc index 17f68cb9..00c3ae55 100644 --- a/.pylintrc +++ b/.pylintrc @@ -67,6 +67,7 @@ disable=missing-module-docstring, method-hidden, too-many-ancestors, duplicate-code, + redefined-builtin, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/README.md b/README.md index 64b25416..defb80c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Uffd -This is the UserFerwaltungsFrontend. -A web service to manage LDAP users, groups and permissions. +Uffd (UserFerwaltungsFrontend) is a web-based user management and single sign-on software. Development chat: [#uffd-development](https://rocket.cccv.de/channel/uffd-development) @@ -10,14 +9,14 @@ Development chat: [#uffd-development](https://rocket.cccv.de/channel/uffd-develo Please note that we refer to Debian packages here and **not** pip packages. - python3 -- python3-ldap3 - python3-flask - python3-flask-sqlalchemy - python3-flask-migrate - python3-qrcode -- python3-fido2 (version 0.5.0, optional) +- python3-fido2 (version 0.5.0 or 0.9.1, optional) - python3-oauthlib - python3-flask-babel +- python3-mysqldb or python3-pymysql for MySQL/MariaDB support Some of the dependencies (especially fido2) changed their API in recent versions, so make sure to install the versions from Debian Buster or Bullseye. For development, you can also use virtualenv with the supplied `requirements.txt`. @@ -34,10 +33,14 @@ FLASK_APP=uffd flask db upgrade FLASK_APP=uffd FLASK_ENV=development flask run ``` -During development, you may want to enable LDAP mocking, as you otherwise need to have access to an actual LDAP server with the required schema. -You can do so by setting `LDAP_SERVICE_MOCK=True` in the config. -Afterwards you can login as a normal user with "testuser" and "userpassword", or as an admin with "testadmin" and "adminpassword". -Please note that the mocked LDAP functionality is very limited and many uffd features do not work correctly without a real LDAP server. +During development, you may want to create some example data: + +``` +FLASK_APP=uffd flask create-examples +``` + +Afterwards you can login as a normal user with "testuser" and "userpassword", or as an admin with "testad +min" and "adminpassword". ## Deployment @@ -62,6 +65,27 @@ The Debian package uses uwsgi to run uffd and ships an `uffd-admin` to execute f If you upgrade, make sure to run `flask db upgrade` after every update! The Debian package takes care of this by itself using uwsgi pre start hooks. For an example uwsgi config, see our [uswgi.ini](uwsgi.ini). You might find our [nginx include file](nginx.include.conf) helpful to setup a web server in front of uwsgi. +## Migration from version 1 + +Prior to version 2 uffd stored users, groups and mail aliases in an LDAP server. +To migrate from version 1 to a later version, make sure to keep the v1 config file as it is with all LDAP settings. +Running the database migrations with `flask db upgrade` automatically imports all users, groups and mail forwardings from LDAP to the database. +Note that all LDAP attributes must be readable, including the password field. +Make sure to have a working backup of the database before running the database upgrade! +Downgrading is not supported. + +After running the migrations you can remove all `LDAP_*`-prefixed settings from the config file except the following ones that are renamed: + +* `LDAP_USER_GID` -> `USER_GID` +* `LDAP_USER_MIN_UID` -> `USER_MIN_UID` +* `LDAP_USER_MAX_UID` -> `USER_MAX_UID` +* `LDAP_USER_SERVICE_MIN_UID` -> `USER_SERVICE_MIN_UID` +* `LDAP_USER_SERVICE_MAX_UID` -> `USER_SERVICE_MAX_UID` +* `LDAP_GROUP_MIN_GID` -> `GROUP_MIN_GID` +* `LDAP_GROUP_MAX_GID` -> `GROUP_MAX_GID` + +Upgrading will not perform any write access to the LDAP server. + ## Python Coding Style Conventions PEP 8 without double new lines, tabs instead of spaces and a max line length of 160 characters. @@ -74,15 +98,6 @@ You can overwrite config variables by creating a config file in the `instance` f The file must be named `config.cfg` (Python syntax), `config.json` or `config.yml`/`config.yaml`. You can also set a custom file name with the environment variable `CONFIG_FILENAME`. -## Bind with LDAP service account or as user? - -Uffd can use a dedicated service account for LDAP operations by setting `LDAP_SERVICE_BIND_DN`. -Leave that variable blank to use anonymous bind. -Or set `LDAP_SERVICE_USER_BIND` to use the credentials of the currently logged in user. - -If you choose to run with user credentials, some features are not available, like password resets -or self signup, since in both cases, no user credentials can exist. - ## OAuth2 Single-Sign-On Provider Other services can use uffd as an OAuth2.0-based authentication provider. @@ -101,7 +116,6 @@ The userinfo endpoint returns json data with the following structure: "name": "Test User", "nickname": "testuser" "email": "testuser@example.com", - "ldap_dn": "uid=testuser,ou=users,dc=example,dc=com", "groups": [ "uffd_access", "users" @@ -109,8 +123,7 @@ The userinfo endpoint returns json data with the following structure: } ``` -`id` is the uidNumber, `name` the display name (cn) and `nickname` the uid of the user's LDAP object. - +`id` is the numeric (Unix) user id, `name` the display name and `nickname` the loginname of the user. ## Translation diff --git a/check_migrations.py b/check_migrations.py index bd151299..9ccf98d6 100755 --- a/check_migrations.py +++ b/check_migrations.py @@ -22,11 +22,11 @@ def run_test(dburi, revision): 'DEBUG': True, 'SQLALCHEMY_DATABASE_URI': dburi, 'SECRET_KEY': 'DEBUGKEY', - 'LDAP_SERVICE_MOCK': True, 'MAIL_SKIP_SEND': True, 'SELF_SIGNUP': True, 'ENABLE_INVITE': True, - 'ENABLE_PASSWORDRESET': True + 'ENABLE_PASSWORDRESET': True, + 'LDAP_SERVICE_MOCK': True } app = create_app(config) with app.test_request_context(): @@ -39,20 +39,20 @@ def run_test(dburi, revision): db.session.add(WebauthnMethod(user=user, name='mywebauthn', cred=b'')) role = Role(name='role', groups={group: RoleGroup(group=group)}) db.session.add(role) - role.members.add(user) - db.session.add(Role(name='base', included_roles=[role], locked=True, is_default=True, moderator_group_dn=group.dn, groups={group: RoleGroup(group=group)})) + role.members.append(user) + db.session.add(Role(name='base', included_roles=[role], locked=True, is_default=True, moderator_group=group, groups={group: RoleGroup(group=group)})) db.session.add(Signup(loginname='newuser', displayname='New User', mail='newuser@example.com', password='newpassword')) - db.session.add(Signup(loginname='testuser', displayname='Testuser', mail='testuser@example.com', password='testpassword', user_dn=user.dn)) + db.session.add(Signup(loginname='testuser', displayname='Testuser', mail='testuser@example.com', password='testpassword', user=user)) invite = Invite(valid_until=datetime.datetime.now(), roles=[role]) db.session.add(invite) invite.signups.append(InviteSignup(loginname='newuser', displayname='New User', mail='newuser@example.com', password='newpassword')) - invite.grants.append(InviteGrant(user_dn=user.dn)) - db.session.add(Invite(creator_dn=user.dn, valid_until=datetime.datetime.now())) - db.session.add(OAuth2Grant(user_dn=user.dn, client_id='testclient', code='testcode', redirect_uri='http://example.com/callback', expires=datetime.datetime.now())) - db.session.add(OAuth2Token(user_dn=user.dn, client_id='testclient', token_type='Bearer', access_token='testcode', refresh_token='testcode', expires=datetime.datetime.now())) - db.session.add(OAuth2DeviceLoginInitiation(oauth2_client_id='testclient', confirmations=[DeviceLoginConfirmation(user_dn=user.dn)])) - db.session.add(PasswordToken(loginname='testuser')) - db.session.add(MailToken(loginname='testuser', newmail='test@example.com')) + invite.grants.append(InviteGrant(user=user)) + db.session.add(Invite(creator=user, valid_until=datetime.datetime.now())) + db.session.add(OAuth2Grant(user=user, client_id='testclient', code='testcode', redirect_uri='http://example.com/callback', expires=datetime.datetime.now())) + db.session.add(OAuth2Token(user=user, client_id='testclient', token_type='Bearer', access_token='testcode', refresh_token='testcode', expires=datetime.datetime.now())) + db.session.add(OAuth2DeviceLoginInitiation(oauth2_client_id='testclient', confirmations=[DeviceLoginConfirmation(user=user)])) + db.session.add(PasswordToken(user=user)) + db.session.add(MailToken(user=user, newmail='test@example.com')) db.session.commit() flask_migrate.downgrade(revision=revision) flask_migrate.upgrade(revision='head') diff --git a/debian/control b/debian/control index 13744d0f..072db47d 100644 --- a/debian/control +++ b/debian/control @@ -18,7 +18,6 @@ Depends: # Unlike most debian python packages, we depend directly on the deb packages and do not want to populate our dependencies from the setup.py . # Because of this we do not use the variable from pybuild. # ${python3:Depends}, - python3-ldap3, python3-flask, python3-flask-sqlalchemy, python3-flask-migrate, @@ -30,4 +29,5 @@ Depends: uwsgi-plugin-python3, Recommends: nginx, -Description: UserFerwaltungsFrontend: Ldap based single sign on and user management web software + python3-mysqldb, +Description: Web-based user management and single sign-on software diff --git a/setup.py b/setup.py index 31353ff9..5f45af54 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ with open('README.md', 'r', encoding='utf-8') as f: setup( name='uffd', version=os.environ.get('PACKAGE_VERSION', 'local'), - description='Ldap based single sign on and user management web software', + description='Web-based user management and single sign-on software', long_description=long_description, long_description_content_type='text/markdown', url='https://git.cccv.de/uffd/uffd', @@ -32,7 +32,6 @@ setup( install_requires=[ # Versions Debian Buster packages are based on. # DO NOT USE FOR PRODUCTION, those in the setup.py are not updated regularly - 'ldap3==2.4.1', 'flask==1.0.2', 'Flask-SQLAlchemy==2.1', 'qrcode==6.1', diff --git a/tests/openldap_ldifs/ldap_server_entries_add.ldif b/tests/openldap_ldifs/ldap_server_entries_add.ldif deleted file mode 100644 index 138d7e8a..00000000 --- a/tests/openldap_ldifs/ldap_server_entries_add.ldif +++ /dev/null @@ -1,42 +0,0 @@ -version: 1 -dn: uid=testuser,ou=users,dc=example,dc=com -objectClass: top -objectClass: organizationalPerson -objectClass: inetOrgPerson -objectClass: person -objectClass: posixAccount -cn: Test User -displayName: Test User -gidNumber: 20001 -givenName: Test User -homeDirectory: /home/testuser -mail: testuser@example.com -sn:: IA== -uid: testuser -uidNumber: 10000 -userPassword: {ssha512}P6mPgcE974bMZkYHnowsXheE74lqtR0HemVUjZxZT7cgPlEhE7fSU1DYEhOx1ZYhOTuE7Ei3EaMFSSoi9Jqf5MHHcjG9oVWL - -dn: uid=testadmin,ou=users,dc=example,dc=com -objectClass: top -objectClass: organizationalPerson -objectClass: inetOrgPerson -objectClass: person -objectClass: posixAccount -cn: Test Admin -displayName: Test Admin -gidNumber: 20001 -givenName: Test Admin -homeDirectory: /home/testadmin -mail: testadmin@example.com -sn:: IA== -uid: testadmin -uidNumber: 10001 -userPassword: {ssha512}SGARsM9lNP9PQ4S+M/pmA7MIDvdyF9WZ8Ki2JvjvxIlMLene5+s+M+Qfi0lfJHOSqucd6CR0F7vDl32rEJNd1ZPCLbCO20pB - -dn: uid=test,ou=postfix,dc=example,dc=com -objectClass: top -objectClass: postfixVirtual -uid: test -mailacceptinggeneralid: test1@example.com -mailacceptinggeneralid: test2@example.com -maildrop: testuser@mail.example.com diff --git a/tests/openldap_ldifs/ldap_server_entries_cleanup.ldif b/tests/openldap_ldifs/ldap_server_entries_cleanup.ldif deleted file mode 100644 index bf80e267..00000000 --- a/tests/openldap_ldifs/ldap_server_entries_cleanup.ldif +++ /dev/null @@ -1,17 +0,0 @@ -uid=testuser,ou=users,dc=example,dc=com -uid=testadmin,ou=users,dc=example,dc=com -uid=newuser,ou=users,dc=example,dc=com -uid=newuser1,ou=users,dc=example,dc=com -uid=newuser2,ou=users,dc=example,dc=com -uid=newuser3,ou=users,dc=example,dc=com -uid=newuser4,ou=users,dc=example,dc=com -uid=newuser5,ou=users,dc=example,dc=com -uid=newuser6,ou=users,dc=example,dc=com -uid=newuser7,ou=users,dc=example,dc=com -uid=newuser8,ou=users,dc=example,dc=com -uid=newuser9,ou=users,dc=example,dc=com -uid=newuser10,ou=users,dc=example,dc=com -uid=newuser11,ou=users,dc=example,dc=com -uid=newuser12,ou=users,dc=example,dc=com -uid=test,ou=postfix,dc=example,dc=com -uid=test1,ou=postfix,dc=example,dc=com diff --git a/tests/openldap_ldifs/ldap_server_entries_modify.ldif b/tests/openldap_ldifs/ldap_server_entries_modify.ldif deleted file mode 100644 index b8894254..00000000 --- a/tests/openldap_ldifs/ldap_server_entries_modify.ldif +++ /dev/null @@ -1,17 +0,0 @@ -version: 1 -dn: cn=users,ou=groups,dc=example,dc=com -changetype: modify -add: uniqueMember -uniqueMember: uid=testuser,ou=users,dc=example,dc=com -uniqueMember: uid=testadmin,ou=users,dc=example,dc=com - -dn: cn=uffd_access,ou=groups,dc=example,dc=com -changetype: modify -add: uniqueMember -uniqueMember: uid=testuser,ou=users,dc=example,dc=com -uniqueMember: uid=testadmin,ou=users,dc=example,dc=com - -dn: cn=uffd_admin,ou=groups,dc=example,dc=com -changetype: modify -add: uniqueMember -uniqueMember: uid=testadmin,ou=users,dc=example,dc=com diff --git a/tests/openldap_mock/ldap_server_entries.json b/tests/openldap_mock/ldap_server_entries.json deleted file mode 100644 index 00d2aaea..00000000 --- a/tests/openldap_mock/ldap_server_entries.json +++ /dev/null @@ -1,349 +0,0 @@ -{ - "entries": [ - { - "dn": "uid=testuser,ou=users,dc=example,dc=com", - "raw": { - "cn": [ - "Test User" - ], - "createTimestamp": [ - "20200101000000Z" - ], - "creatorsName": [ - "cn=admin,dc=example,dc=com" - ], - "displayName": [ - "Test User" - ], - "entryDN": [ - "uid=testuser,ou=users,dc=example,dc=com" - ], - "entryUUID": [ - "75e62c6a-03c2-11eb-adc1-0242ac120002" - ], - "gidNumber": [ - "20001" - ], - "givenName": [ - "Test User" - ], - "hasSubordinates": [ - "FALSE" - ], - "homeDirectory": [ - "/home/testuser" - ], - "mail": [ - "testuser@example.com" - ], - "memberOf": [ - "cn=uffd_access,ou=groups,dc=example,dc=com", - "cn=users,ou=groups,dc=example,dc=com" - ], - "modifiersName": [ - "cn=admin,dc=example,dc=com" - ], - "modifyTimestamp": [ - "20200101000000Z" - ], - "objectClass": [ - "top", - "inetOrgPerson", - "organizationalPerson", - "person", - "posixAccount" - ], - "sn": [ - " " - ], - "structuralObjectClass": [ - "inetOrgPerson" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "uid": [ - "testuser" - ], - "uidNumber": [ - "10000" - ], - "userPassword": [ - "userpassword" - ] - } - }, - { - "dn": "uid=testadmin,ou=users,dc=example,dc=com", - "raw": { - "cn": [ - "Test Admin" - ], - "createTimestamp": [ - "20200101000000Z" - ], - "creatorsName": [ - "cn=admin,dc=example,dc=com" - ], - "displayName": [ - "Test Admin" - ], - "entryDN": [ - "uid=testadmin,ou=users,dc=example,dc=com" - ], - "entryUUID": [ - "678c8470-03c2-11eb-adc1-0242ac120002" - ], - "gidNumber": [ - "20001" - ], - "givenName": [ - "Test Admin" - ], - "hasSubordinates": [ - "FALSE" - ], - "homeDirectory": [ - "/home/testadmin" - ], - "mail": [ - "testadmin@example.com" - ], - "memberOf": [ - "cn=users,ou=groups,dc=example,dc=com", - "cn=uffd_access,ou=groups,dc=example,dc=com", - "cn=uffd_admin,ou=groups,dc=example,dc=com" - ], - "modifiersName": [ - "cn=admin,dc=example,dc=com" - ], - "modifyTimestamp": [ - "20200101000000Z" - ], - "objectClass": [ - "top", - "inetOrgPerson", - "organizationalPerson", - "person", - "posixAccount" - ], - "sn": [ - " " - ], - "structuralObjectClass": [ - "inetOrgPerson" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "uid": [ - "testadmin" - ], - "uidNumber": [ - "10001" - ], - "userPassword": [ - "adminpassword" - ] - } - }, - { - "dn": "cn=users,ou=groups,dc=example,dc=com", - "raw": { - "cn": [ - "users" - ], - "createTimestamp": [ - "20200101000000Z" - ], - "creatorsName": [ - "cn=admin,dc=example,dc=com" - ], - "description": [ - "Base group for all users" - ], - "entryDN": [ - "cn=users,ou=groups,dc=example,dc=com" - ], - "entryUUID": [ - "1aec0e8c-03c3-11eb-adc1-0242ac120002" - ], - "gidNumber": [ - "20001" - ], - "hasSubordinates": [ - "FALSE" - ], - "modifiersName": [ - "cn=admin,dc=example,dc=com" - ], - "modifyTimestamp": [ - "20200101000000Z" - ], - "objectClass": [ - "posixGroup", - "groupOfUniqueNames", - "top" - ], - "structuralObjectClass": [ - "groupOfUniqueNames" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "uniqueMember": [ - "cn=dummy,ou=system,dc=example,dc=com", - "uid=testuser,ou=users,dc=example,dc=com", - "uid=testadmin,ou=users,dc=example,dc=com" - ] - } - }, - { - "dn": "cn=uffd_access,ou=groups,dc=example,dc=com", - "raw": { - "cn": [ - "uffd_access" - ], - "createTimestamp": [ - "20200101000000Z" - ], - "creatorsName": [ - "cn=admin,dc=example,dc=com" - ], - "description": [ - "User access to uffd selfservice" - ], - "entryDN": [ - "cn=uffd_access,ou=groups,dc=example,dc=com" - ], - "entryUUID": [ - "4fc8dd60-03c3-11eb-adc1-0242ac120002" - ], - "gidNumber": [ - "20002" - ], - "hasSubordinates": [ - "FALSE" - ], - "modifiersName": [ - "cn=admin,dc=example,dc=com" - ], - "modifyTimestamp": [ - "20200101000000Z" - ], - "objectClass": [ - "posixGroup", - "groupOfUniqueNames", - "top" - ], - "structuralObjectClass": [ - "groupOfUniqueNames" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "uniqueMember": [ - "cn=dummy,ou=system,dc=example,dc=com", - "uid=testuser,ou=users,dc=example,dc=com", - "uid=testadmin,ou=users,dc=example,dc=com" - ] - } - }, - { - "dn": "cn=uffd_admin,ou=groups,dc=example,dc=com", - "raw": { - "cn": [ - "uffd_admin" - ], - "createTimestamp": [ - "20200101000000Z" - ], - "creatorsName": [ - "cn=admin,dc=example,dc=com" - ], - "description": [ - "Admin access to uffd selfservice" - ], - "entryDN": [ - "cn=uffd_admin,ou=groups,dc=example,dc=com" - ], - "entryUUID": [ - "b5d869d6-03c3-11eb-adc1-0242ac120002" - ], - "gidNumber": [ - "20003" - ], - "hasSubordinates": [ - "FALSE" - ], - "modifiersName": [ - "cn=admin,dc=example,dc=com" - ], - "modifyTimestamp": [ - "20200101000000Z" - ], - "objectClass": [ - "posixGroup", - "groupOfUniqueNames", - "top" - ], - "structuralObjectClass": [ - "groupOfUniqueNames" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "uniqueMember": [ - "cn=dummy,ou=system,dc=example,dc=com", - "uid=testadmin,ou=users,dc=example,dc=com" - ] - } - }, - { - "dn": "uid=test,ou=postfix,dc=example,dc=com", - "raw": { - "createTimestamp": [ - "20200101000000Z" - ], - "creatorsName": [ - "cn=admin,dc=example,dc=com" - ], - "entryDN": [ - "uid=test,ou=postfix,dc=example,dc=com" - ], - "entryUUID": [ - "926e5273-a545-4dfe-8f20-d1eeaf41d796" - ], - "hasSubordinates": [ - "FALSE" - ], - "mailacceptinggeneralid": [ - "test1@example.com", - "test2@example.com" - ], - "maildrop": [ - "testuser@mail.example.com" - ], - "modifiersName": [ - "cn=admin,dc=example,dc=com" - ], - "modifyTimestamp": [ - "20200101000000Z" - ], - "objectClass": [ - "top", - "postfixVirtual" - ], - "structuralObjectClass": [ - "postfixVirtual" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "uid": [ - "test" - ] - } - } - ] -} diff --git a/tests/openldap_mock/ldap_server_info.json b/tests/openldap_mock/ldap_server_info.json deleted file mode 100644 index 700ec931..00000000 --- a/tests/openldap_mock/ldap_server_info.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "raw": { - "altServer": [], - "configContext": [ - "cn=config" - ], - "entryDN": [ - "" - ], - "namingContexts": [ - "dc=example,dc=com" - ], - "objectClass": [ - "top", - "OpenLDAProotDSE" - ], - "structuralObjectClass": [ - "OpenLDAProotDSE" - ], - "subschemaSubentry": [ - "cn=Subschema" - ], - "supportedCapabilities": [], - "supportedControl": [ - "2.16.840.1.113730.3.4.18", - "2.16.840.1.113730.3.4.2", - "1.3.6.1.4.1.4203.1.10.1", - "1.3.6.1.1.22", - "1.2.840.113556.1.4.319", - "1.2.826.0.1.3344810.2.3", - "1.3.6.1.1.13.2", - "1.3.6.1.1.13.1", - "1.3.6.1.1.12" - ], - "supportedExtension": [ - "1.3.6.1.4.1.1466.20037", - "1.3.6.1.4.1.4203.1.11.1", - "1.3.6.1.4.1.4203.1.11.3", - "1.3.6.1.1.8" - ], - "supportedFeatures": [ - "1.3.6.1.1.14", - "1.3.6.1.4.1.4203.1.5.1", - "1.3.6.1.4.1.4203.1.5.2", - "1.3.6.1.4.1.4203.1.5.3", - "1.3.6.1.4.1.4203.1.5.4", - "1.3.6.1.4.1.4203.1.5.5" - ], - "supportedLDAPVersion": [ - "3" - ], - "supportedSASLMechanisms": [ - "DIGEST-MD5", - "CRAM-MD5", - "NTLM" - ], - "vendorName": [], - "vendorVersion": [] - }, - "type": "DsaInfo" -} diff --git a/tests/openldap_mock/ldap_server_schema.json b/tests/openldap_mock/ldap_server_schema.json deleted file mode 100644 index 91957d19..00000000 --- a/tests/openldap_mock/ldap_server_schema.json +++ /dev/null @@ -1,519 +0,0 @@ -{ - "raw": { - "attributeTypes": [ - "( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.21.9 NAME 'structuralObjectClass' DESC 'RFC4512: structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.1 NAME 'createTimestamp' DESC 'RFC4512: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC4512: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.3 NAME 'creatorsName' DESC 'RFC4512: name of creator' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.4 NAME 'modifiersName' DESC 'RFC4512: name of last modifier' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.9 NAME 'hasSubordinates' DESC 'X.501: entry has children' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC4512: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 1.3.6.1.1.20 NAME 'entryDN' DESC 'DN of the entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 1.3.6.1.1.16.4 NAME 'entryUUID' DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )", - "( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' DESC 'RFC4512: alternative servers' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC4512: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' DESC 'RFC4512: supported controls' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' DESC 'RFC4512: supported extended operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC4512: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' DESC 'RFC4512: supported SASL mechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )", - "( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' DESC 'RFC4512: features supported by the server' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )", - "( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )", - "( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )", - "( 2.5.21.4 NAME 'matchingRules' DESC 'RFC4512: matching rules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )", - "( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC4512: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )", - "( 2.5.21.6 NAME 'objectClasses' DESC 'RFC4512: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )", - "( 2.5.21.8 NAME 'matchingRuleUse' DESC 'RFC4512: matching rule uses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )", - "( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )", - "( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) DESC 'RFC4512: name of aliased object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'RFC3296: subordinate referral URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation )", - "( 1.3.6.1.4.1.1466.101.119.3 NAME 'entryTtl' DESC 'RFC2589: entry time-to-live' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )", - "( 1.3.6.1.4.1.1466.101.119.4 NAME 'dynamicSubtrees' DESC 'RFC2589: dynamic subtrees' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION USAGE dSAOperation )", - "( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC4519: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 2.5.4.41 NAME 'name' DESC 'RFC4519: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )", - "( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name )", - "( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 1.3.6.1.1.1.1.0 NAME 'uidNumber' DESC 'RFC2307: An integer uniquely identifying a user in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.1 NAME 'gidNumber' DESC 'RFC2307: An integer uniquely identifying a group in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 2.5.4.35 NAME 'userPassword' DESC 'RFC4519/2307: password of user' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} )", - "( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'RFC2079: Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.4.13 NAME 'description' DESC 'RFC4519: descriptive information' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )", - "( 2.5.4.34 NAME 'seeAlso' DESC 'RFC4519: DN of related object' SUP distinguishedName )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.78 NAME 'olcConfigFile' DESC 'File for slapd configuration directives' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.79 NAME 'olcConfigDir' DESC 'Directory for slapd configuration backend' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.1 NAME 'olcAccess' DESC 'Access Control List' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.86 NAME 'olcAddContentAcl' DESC 'Check ACLs against content of Add ops' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.2 NAME 'olcAllows' DESC 'Allowed set of deprecated features' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.3 NAME 'olcArgsFile' DESC 'File for slapd command line options' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.5 NAME 'olcAttributeOptions' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.4 NAME 'olcAttributeTypes' DESC 'OpenLDAP attributeTypes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.6 NAME 'olcAuthIDRewrite' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.7 NAME 'olcAuthzPolicy' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.8 NAME 'olcAuthzRegexp' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.9 NAME 'olcBackend' DESC 'A type of backend' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORDERED 'SIBLINGS' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.10 NAME 'olcConcurrency' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.11 NAME 'olcConnMaxPending' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.12 NAME 'olcConnMaxPendingAuth' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.13 NAME 'olcDatabase' DESC 'The backend type for a database instance' SUP olcBackend SINGLE-VALUE X-ORDERED 'SIBLINGS' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.14 NAME 'olcDefaultSearchBase' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.15 NAME 'olcDisallows' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.16 NAME 'olcDitContentRules' DESC 'OpenLDAP DIT content rules' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.20 NAME 'olcExtraAttrs' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.17 NAME 'olcGentleHUP' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.17 NAME 'olcHidden' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.18 NAME 'olcIdleTimeout' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.19 NAME 'olcInclude' SUP labeledURI )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.20 NAME 'olcIndexSubstrIfMinLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.21 NAME 'olcIndexSubstrIfMaxLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.22 NAME 'olcIndexSubstrAnyLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.23 NAME 'olcIndexSubstrAnyStep' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.84 NAME 'olcIndexIntLen' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.4 NAME 'olcLastMod' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.85 NAME 'olcLdapSyntaxes' DESC 'OpenLDAP ldapSyntax' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.5 NAME 'olcLimits' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.93 NAME 'olcListenerThreads' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.26 NAME 'olcLocalSSF' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.27 NAME 'olcLogFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.28 NAME 'olcLogLevel' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.6 NAME 'olcMaxDerefDepth' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.16 NAME 'olcMirrorMode' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.30 NAME 'olcModuleLoad' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.31 NAME 'olcModulePath' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.18 NAME 'olcMonitoring' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.32 NAME 'olcObjectClasses' DESC 'OpenLDAP object classes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.33 NAME 'olcObjectIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.34 NAME 'olcOverlay' SUP olcDatabase SINGLE-VALUE X-ORDERED 'SIBLINGS' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.35 NAME 'olcPasswordCryptSaltFormat' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.36 NAME 'olcPasswordHash' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.37 NAME 'olcPidFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.38 NAME 'olcPlugin' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.39 NAME 'olcPluginLogFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.40 NAME 'olcReadOnly' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.41 NAME 'olcReferral' SUP labeledURI SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.7 NAME 'olcReplica' SUP labeledURI EQUALITY caseIgnoreMatch X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.43 NAME 'olcReplicaArgsFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.44 NAME 'olcReplicaPidFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.45 NAME 'olcReplicationInterval' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.46 NAME 'olcReplogFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.47 NAME 'olcRequires' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.48 NAME 'olcRestrict' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.49 NAME 'olcReverseLookup' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.8 NAME 'olcRootDN' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.51 NAME 'olcRootDSE' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.9 NAME 'olcRootPW' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.89 NAME 'olcSaslAuxprops' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.53 NAME 'olcSaslHost' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.54 NAME 'olcSaslRealm' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.56 NAME 'olcSaslSecProps' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.58 NAME 'olcSchemaDN' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.59 NAME 'olcSecurity' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.81 NAME 'olcServerID' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.60 NAME 'olcSizeLimit' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.61 NAME 'olcSockbufMaxIncoming' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.62 NAME 'olcSockbufMaxIncomingAuth' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.83 NAME 'olcSortVals' DESC 'Attributes whose values will always be sorted' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.15 NAME 'olcSubordinate' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.10 NAME 'olcSuffix' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.19 NAME 'olcSyncUseSubentry' DESC 'Store sync context in a subentry' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.11 NAME 'olcSyncrepl' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORDERED 'VALUES' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.90 NAME 'olcTCPBuffer' DESC 'Custom TCP buffer size' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.66 NAME 'olcThreads' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.67 NAME 'olcTimeLimit' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.68 NAME 'olcTLSCACertificateFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.69 NAME 'olcTLSCACertificatePath' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.70 NAME 'olcTLSCertificateFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.71 NAME 'olcTLSCertificateKeyFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.72 NAME 'olcTLSCipherSuite' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.73 NAME 'olcTLSCRLCheck' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.82 NAME 'olcTLSCRLFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.74 NAME 'olcTLSRandFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.75 NAME 'olcTLSVerifyClient' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.77 NAME 'olcTLSDHParamFile' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.87 NAME 'olcTLSProtocolMin' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.80 NAME 'olcToolThreads' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.12 NAME 'olcUpdateDN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.13 NAME 'olcUpdateRef' SUP labeledURI EQUALITY caseIgnoreMatch )", - "( 1.3.6.1.4.1.4203.1.12.2.3.0.88 NAME 'olcWriteTimeout' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.1 NAME 'olcDbDirectory' DESC 'Directory for database content' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.1.2 NAME 'olcDbCheckpoint' DESC 'Database checkpoint interval in kbytes and minutes' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.1.4 NAME 'olcDbNoSync' DESC 'Disable synchronous database writes' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.3 NAME 'olcDbEnvFlags' DESC 'Database environment flags' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.2 NAME 'olcDbIndex' DESC 'Attribute index parameters' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.1 NAME 'olcDbMaxReaders' DESC 'Maximum number of threads that may access the DB concurrently' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.2 NAME 'olcDbMaxSize' DESC 'Maximum size of DB in bytes' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.0.3 NAME 'olcDbMode' DESC 'Unix permissions of database files' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.12.5 NAME 'olcDbRtxnSize' DESC 'Number of entries to process in one read transaction' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.2.1.9 NAME 'olcDbSearchStack' DESC 'Depth of search stack in IDLs' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.2.840.113556.1.2.102 NAME 'memberOf' DESC 'Group that the entry belongs to' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation X-ORIGIN 'iPlanet Delegated Administrator' )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.0 NAME 'olcMemberOfDN' DESC 'DN to be used as modifiersName' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.1 NAME 'olcMemberOfDangling' DESC 'Behavior with respect to dangling members, constrained to ignore, drop, error' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.2 NAME 'olcMemberOfRefInt' DESC 'Take care of referential integrity' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.3 NAME 'olcMemberOfGroupOC' DESC 'Group objectClass' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.4 NAME 'olcMemberOfMemberAD' DESC 'member attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.5 NAME 'olcMemberOfMemberOfAD' DESC 'memberOf attribute' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.18.7 NAME 'olcMemberOfDanglingError' DESC 'Error code returned in case of dangling back reference' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.10.1 NAME 'olcUniqueBase' DESC 'Subtree for uniqueness searches' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.10.2 NAME 'olcUniqueIgnore' DESC 'Attributes for which uniqueness shall not be enforced' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.10.3 NAME 'olcUniqueAttribute' DESC 'Attributes for which uniqueness shall be enforced' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.10.4 NAME 'olcUniqueStrict' DESC 'Enforce uniqueness of null values' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.10.5 NAME 'olcUniqueURI' DESC 'List of keywords and LDAP URIs for a uniqueness domain' EQUALITY caseExactMatch ORDERING caseExactOrderingMatch SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.11.1 NAME 'olcRefintAttribute' DESC 'Attributes for referential integrity' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.11.2 NAME 'olcRefintNothing' DESC 'Replacement DN to supply when needed' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 1.3.6.1.4.1.4203.1.12.2.3.3.11.3 NAME 'olcRefintModifiersName' DESC 'The DN to use as modifiersName' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )", - "( 2.5.4.2 NAME 'knowledgeInformation' DESC 'RFC2256: knowledge information' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )", - "( 2.5.4.4 NAME ( 'sn' 'surname' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name )", - "( 2.5.4.5 NAME 'serialNumber' DESC 'RFC2256: serial number of the entity' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64} )", - "( 2.5.4.6 NAME ( 'c' 'countryName' ) DESC 'RFC4519: two-letter ISO-3166 country code' SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 SINGLE-VALUE )", - "( 2.5.4.7 NAME ( 'l' 'localityName' ) DESC 'RFC2256: locality which this object resides in' SUP name )", - "( 2.5.4.8 NAME ( 'st' 'stateOrProvinceName' ) DESC 'RFC2256: state or province which this object resides in' SUP name )", - "( 2.5.4.9 NAME ( 'street' 'streetAddress' ) DESC 'RFC2256: street address of this object' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )", - "( 2.5.4.10 NAME ( 'o' 'organizationName' ) DESC 'RFC2256: organization this object belongs to' SUP name )", - "( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name )", - "( 2.5.4.12 NAME 'title' DESC 'RFC2256: title associated with the entity' SUP name )", - "( 2.5.4.14 NAME 'searchGuide' DESC 'RFC2256: search guide, deprecated by enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 )", - "( 2.5.4.15 NAME 'businessCategory' DESC 'RFC2256: business category' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )", - "( 2.5.4.16 NAME 'postalAddress' DESC 'RFC2256: postal address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 2.5.4.17 NAME 'postalCode' DESC 'RFC2256: postal code' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} )", - "( 2.5.4.18 NAME 'postOfficeBox' DESC 'RFC2256: Post Office Box' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} )", - "( 2.5.4.19 NAME 'physicalDeliveryOfficeName' DESC 'RFC2256: Physical Delivery Office Name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )", - "( 2.5.4.20 NAME 'telephoneNumber' DESC 'RFC2256: Telephone Number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32} )", - "( 2.5.4.21 NAME 'telexNumber' DESC 'RFC2256: Telex Number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 )", - "( 2.5.4.22 NAME 'teletexTerminalIdentifier' DESC 'RFC2256: Teletex Terminal Identifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 )", - "( 2.5.4.23 NAME ( 'facsimileTelephoneNumber' 'fax' ) DESC 'RFC2256: Facsimile (Fax) Telephone Number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )", - "( 2.5.4.24 NAME 'x121Address' DESC 'RFC2256: X.121 Address' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{15} )", - "( 2.5.4.25 NAME 'internationaliSDNNumber' DESC 'RFC2256: international ISDN number' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{16} )", - "( 2.5.4.26 NAME 'registeredAddress' DESC 'RFC2256: registered postal address' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 2.5.4.27 NAME 'destinationIndicator' DESC 'RFC2256: destination indicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{128} )", - "( 2.5.4.28 NAME 'preferredDeliveryMethod' DESC 'RFC2256: preferred delivery method' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-VALUE )", - "( 2.5.4.29 NAME 'presentationAddress' DESC 'RFC2256: presentation address' EQUALITY presentationAddressMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.43 SINGLE-VALUE )", - "( 2.5.4.30 NAME 'supportedApplicationContext' DESC 'RFC2256: supported application context' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.4.31 NAME 'member' DESC 'RFC2256: member of a group' SUP distinguishedName )", - "( 2.5.4.32 NAME 'owner' DESC 'RFC2256: owner (of the object)' SUP distinguishedName )", - "( 2.5.4.33 NAME 'roleOccupant' DESC 'RFC2256: occupant of role' SUP distinguishedName )", - "( 2.5.4.36 NAME 'userCertificate' DESC 'RFC2256: X.509 user certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )", - "( 2.5.4.37 NAME 'cACertificate' DESC 'RFC2256: X.509 CA certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )", - "( 2.5.4.38 NAME 'authorityRevocationList' DESC 'RFC2256: X.509 authority revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )", - "( 2.5.4.39 NAME 'certificateRevocationList' DESC 'RFC2256: X.509 certificate revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )", - "( 2.5.4.40 NAME 'crossCertificatePair' DESC 'RFC2256: X.509 cross certificate pair, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 )", - "( 2.5.4.42 NAME ( 'givenName' 'gn' ) DESC 'RFC2256: first name(s) for which the entity is known by' SUP name )", - "( 2.5.4.43 NAME 'initials' DESC 'RFC2256: initials of some or all of names, but not the surname(s).' SUP name )", - "( 2.5.4.44 NAME 'generationQualifier' DESC 'RFC2256: name qualifier indicating a generation' SUP name )", - "( 2.5.4.45 NAME 'x500UniqueIdentifier' DESC 'RFC2256: X.500 unique identifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", - "( 2.5.4.46 NAME 'dnQualifier' DESC 'RFC2256: DN qualifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )", - "( 2.5.4.47 NAME 'enhancedSearchGuide' DESC 'RFC2256: enhanced search guide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 )", - "( 2.5.4.48 NAME 'protocolInformation' DESC 'RFC2256: protocol information' EQUALITY protocolInformationMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.42 )", - "( 2.5.4.50 NAME 'uniqueMember' DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )", - "( 2.5.4.51 NAME 'houseIdentifier' DESC 'RFC2256: house identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )", - "( 2.5.4.52 NAME 'supportedAlgorithms' DESC 'RFC2256: supported algorithms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 )", - "( 2.5.4.53 NAME 'deltaRevocationList' DESC 'RFC2256: delta revocation list; use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )", - "( 2.5.4.54 NAME 'dmdName' DESC 'RFC2256: name of DMD' SUP name )", - "( 2.5.4.65 NAME 'pseudonym' DESC 'X.520(4th): pseudonym for the object' SUP name )", - "( 0.9.2342.19200300.100.1.3 NAME ( 'mail' 'rfc822Mailbox' ) DESC 'RFC1274: RFC822 Mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", - "( 0.9.2342.19200300.100.1.25 NAME ( 'dc' 'domainComponent' ) DESC 'RFC1274/2247: domain component' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' DESC 'RFC1274: domain associated with object' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.2.840.113549.1.9.1 NAME ( 'email' 'emailAddress' 'pkcs9email' ) DESC 'RFC3280: legacy attribute for email addresses in DNs' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )", - "( 0.9.2342.19200300.100.1.2 NAME 'textEncodedORAddress' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.4 NAME 'info' DESC 'RFC1274: general information' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} )", - "( 0.9.2342.19200300.100.1.5 NAME ( 'drink' 'favouriteDrink' ) DESC 'RFC1274: favorite drink' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' DESC 'RFC1274: room number' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.7 NAME 'photo' DESC 'RFC1274: photo (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23{25000} )", - "( 0.9.2342.19200300.100.1.8 NAME 'userClass' DESC 'RFC1274: category of user' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.9 NAME 'host' DESC 'RFC1274: host computer' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.10 NAME 'manager' DESC 'RFC1274: DN of manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' DESC 'RFC1274: unique identifier of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' DESC 'RFC1274: title of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' DESC 'RFC1274: version of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' DESC 'RFC1274: DN of author of document' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' DESC 'RFC1274: location of document original' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.20 NAME ( 'homePhone' 'homeTelephoneNumber' ) DESC 'RFC1274: home telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 0.9.2342.19200300.100.1.21 NAME 'secretary' DESC 'RFC1274: DN of secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.22 NAME 'otherMailbox' SYNTAX 1.3.6.1.4.1.1466.115.121.1.39 )", - "( 0.9.2342.19200300.100.1.26 NAME 'aRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.27 NAME 'mDRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.28 NAME 'mXRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.29 NAME 'nSRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.30 NAME 'sOARecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.31 NAME 'cNAMERecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 0.9.2342.19200300.100.1.38 NAME 'associatedName' DESC 'RFC1274: DN of entry associated with domain' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' DESC 'RFC1274: home postal address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' DESC 'RFC1274: personal title' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.41 NAME ( 'mobile' 'mobileTelephoneNumber' ) DESC 'RFC1274: mobile telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 0.9.2342.19200300.100.1.42 NAME ( 'pager' 'pagerTelephoneNumber' ) DESC 'RFC1274: pager telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 0.9.2342.19200300.100.1.43 NAME ( 'co' 'friendlyCountryName' ) DESC 'RFC1274: friendly country name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' DESC 'RFC1274: unique identifer' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' DESC 'RFC1274: organizational status' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.46 NAME 'janetMailbox' DESC 'RFC1274: Janet mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", - "( 0.9.2342.19200300.100.1.47 NAME 'mailPreferenceOption' DESC 'RFC1274: mail preference option' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 0.9.2342.19200300.100.1.48 NAME 'buildingName' DESC 'RFC1274: name of building' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )", - "( 0.9.2342.19200300.100.1.49 NAME 'dSAQuality' DESC 'RFC1274: DSA Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.19 SINGLE-VALUE )", - "( 0.9.2342.19200300.100.1.50 NAME 'singleLevelQuality' DESC 'RFC1274: Single Level Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE )", - "( 0.9.2342.19200300.100.1.51 NAME 'subtreeMinimumQuality' DESC 'RFC1274: Subtree Mininum Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE )", - "( 0.9.2342.19200300.100.1.52 NAME 'subtreeMaximumQuality' DESC 'RFC1274: Subtree Maximun Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE )", - "( 0.9.2342.19200300.100.1.53 NAME 'personalSignature' DESC 'RFC1274: Personal Signature (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23 )", - "( 0.9.2342.19200300.100.1.54 NAME 'dITRedirect' DESC 'RFC1274: DIT Redirect' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 0.9.2342.19200300.100.1.55 NAME 'audio' DESC 'RFC1274: audio (u-law)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.4{25000} )", - "( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' DESC 'RFC1274: publisher of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; the common name' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to the login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Netgroup triple' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' DESC 'Service port number' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' DESC 'Service protocol name' SUP name )", - "( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' DESC 'IP protocol number' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' DESC 'ONC RPC number' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IPv4 addresses as a dotted decimal omitting leading zeros or IPv6 addresses as defined in RFC2373' SUP name )", - "( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP network as a dotted decimal, eg. 192.168, omitting leading zeros' SUP name SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP netmask as a dotted decimal, eg. 255.255.255.0, omitting leading zeros' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.bootparamd parameter' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image name' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.26 NAME 'nisMapName' DESC 'Name of a A generic NIS map' SUP name )", - "( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' DESC 'A generic NIS entry' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey' DESC 'NIS public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey' DESC 'NIS secret key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.30 NAME 'nisDomain' DESC 'NIS domain' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.1.1.1.31 NAME 'automountMapName' DESC 'automount Map Name' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.32 NAME 'automountKey' DESC 'Automount Key value' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 1.3.6.1.1.1.1.33 NAME 'automountInformation' DESC 'Automount information' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )", - "( 2.16.840.1.113730.3.1.1 NAME 'carLicense' DESC 'RFC2798: vehicle license or registration plate' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.16.840.1.113730.3.1.2 NAME 'departmentNumber' DESC 'RFC2798: identifies a department within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.16.840.1.113730.3.1.241 NAME 'displayName' DESC 'RFC2798: preferred name to be used when displaying entries' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC 'RFC2798: numerically identifies an employee within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 2.16.840.1.113730.3.1.4 NAME 'employeeType' DESC 'RFC2798: type of employment for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 0.9.2342.19200300.100.1.60 NAME 'jpegPhoto' DESC 'RFC2798: a JPEG image' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )", - "( 2.16.840.1.113730.3.1.39 NAME 'preferredLanguage' DESC 'RFC2798: preferred written or spoken language for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )", - "( 2.16.840.1.113730.3.1.40 NAME 'userSMIMECertificate' DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )", - "( 2.16.840.1.113730.3.1.216 NAME 'userPKCS12' DESC 'RFC2798: personal identity information, a PKCS #12 PFX' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )", - "( 1.3.6.1.4.1.4203.666.1.200 NAME 'mailacceptinggeneralid' DESC 'Postfix mail local address alias attribute' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", - "( 1.3.6.1.4.1.4203.666.1.201 NAME 'maildrop' DESC 'Postfix mail final destination attribute' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )" - ], - "cn": [ - "Subschema" - ], - "createTimestamp": [ - "20200930024551Z" - ], - "dITContentRules": [], - "dITStructureRules": [], - "entryDN": [ - "cn=Subschema" - ], - "ldapSyntaxes": [ - "( 1.3.6.1.4.1.1466.115.121.1.4 DESC 'Audio' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.5 DESC 'Binary' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.6 DESC 'Bit String' )", - "( 1.3.6.1.4.1.1466.115.121.1.7 DESC 'Boolean' )", - "( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.9 DESC 'Certificate List' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.10 DESC 'Certificate Pair' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.4203.666.11.10.2.1 DESC 'X.509 AttributeCertificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'Distinguished Name' )", - "( 1.2.36.79672281.1.5.0 DESC 'RDN' )", - "( 1.3.6.1.4.1.1466.115.121.1.14 DESC 'Delivery Method' )", - "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )", - "( 1.3.6.1.4.1.1466.115.121.1.22 DESC 'Facsimile Telephone Number' )", - "( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )", - "( 1.3.6.1.4.1.1466.115.121.1.26 DESC 'IA5 String' )", - "( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )", - "( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )", - "( 1.3.6.1.4.1.1466.115.121.1.36 DESC 'Numeric String' )", - "( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )", - "( 1.3.6.1.4.1.1466.115.121.1.39 DESC 'Other Mailbox' )", - "( 1.3.6.1.4.1.1466.115.121.1.40 DESC 'Octet String' )", - "( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' )", - "( 1.3.6.1.4.1.1466.115.121.1.44 DESC 'Printable String' )", - "( 1.3.6.1.4.1.1466.115.121.1.11 DESC 'Country String' )", - "( 1.3.6.1.4.1.1466.115.121.1.45 DESC 'SubtreeSpecification' )", - "( 1.3.6.1.4.1.1466.115.121.1.49 DESC 'Supported Algorithm' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )", - "( 1.3.6.1.4.1.1466.115.121.1.50 DESC 'Telephone Number' )", - "( 1.3.6.1.4.1.1466.115.121.1.52 DESC 'Telex Number' )", - "( 1.3.6.1.1.1.0.0 DESC 'RFC2307 NIS Netgroup Triple' )", - "( 1.3.6.1.1.1.0.1 DESC 'RFC2307 Boot Parameter' )", - "( 1.3.6.1.1.16.1 DESC 'UUID' )" - ], - "matchingRuleUse": [ - "( 1.2.840.113556.1.4.804 NAME 'integerBitOrMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", - "( 1.2.840.113556.1.4.803 NAME 'integerBitAndMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", - "( 1.3.6.1.4.1.1466.109.114.2 NAME 'caseIgnoreIA5Match' APPLIES ( altServer $ c $ mail $ dc $ associatedDomain $ email $ aRecord $ mDRecord $ mXRecord $ nSRecord $ sOARecord $ cNAMERecord $ janetMailbox $ gecos $ homeDirectory $ loginShell $ memberUid $ memberNisNetgroup $ nisNetgroupTriple $ ipNetmaskNumber $ macAddress $ bootParameter $ bootFile $ nisMapEntry $ nisDomain $ automountMapName $ automountKey $ automountInformation $ mailacceptinggeneralid $ maildrop ) )", - "( 1.3.6.1.4.1.1466.109.114.1 NAME 'caseExactIA5Match' APPLIES ( altServer $ c $ mail $ dc $ associatedDomain $ email $ aRecord $ mDRecord $ mXRecord $ nSRecord $ sOARecord $ cNAMERecord $ janetMailbox $ gecos $ homeDirectory $ loginShell $ memberUid $ memberNisNetgroup $ nisNetgroupTriple $ ipNetmaskNumber $ macAddress $ bootParameter $ bootFile $ nisMapEntry $ nisDomain $ automountMapName $ automountKey $ automountInformation $ mailacceptinggeneralid $ maildrop ) )", - "( 2.5.13.38 NAME 'certificateListExactMatch' APPLIES ( authorityRevocationList $ certificateRevocationList $ deltaRevocationList ) )", - "( 2.5.13.34 NAME 'certificateExactMatch' APPLIES ( userCertificate $ cACertificate ) )", - "( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' APPLIES ( supportedControl $ supportedExtension $ supportedFeatures $ ldapSyntaxes $ supportedApplicationContext ) )", - "( 2.5.13.29 NAME 'integerFirstComponentMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", - "( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' APPLIES ( createTimestamp $ modifyTimestamp ) )", - "( 2.5.13.27 NAME 'generalizedTimeMatch' APPLIES ( createTimestamp $ modifyTimestamp ) )", - "( 2.5.13.24 NAME 'protocolInformationMatch' APPLIES protocolInformation )", - "( 2.5.13.23 NAME 'uniqueMemberMatch' APPLIES uniqueMember )", - "( 2.5.13.22 NAME 'presentationAddressMatch' APPLIES presentationAddress )", - "( 2.5.13.20 NAME 'telephoneNumberMatch' APPLIES ( telephoneNumber $ homePhone $ mobile $ pager ) )", - "( 2.5.13.18 NAME 'octetStringOrderingMatch' APPLIES ( userPassword $ nisPublicKey $ nisSecretKey ) )", - "( 2.5.13.17 NAME 'octetStringMatch' APPLIES ( userPassword $ nisPublicKey $ nisSecretKey ) )", - "( 2.5.13.16 NAME 'bitStringMatch' APPLIES x500UniqueIdentifier )", - "( 2.5.13.15 NAME 'integerOrderingMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", - "( 2.5.13.14 NAME 'integerMatch' APPLIES ( supportedLDAPVersion $ entryTtl $ uidNumber $ gidNumber $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcIdleTimeout $ olcIndexSubstrIfMinLen $ olcIndexSubstrIfMaxLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcMaxDerefDepth $ olcReplicationInterval $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcThreads $ olcToolThreads $ olcWriteTimeout $ olcDbMaxReaders $ olcDbMaxSize $ olcDbRtxnSize $ olcDbSearchStack $ mailPreferenceOption $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ ipServicePort $ ipProtocolNumber $ oncRpcNumber ) )", - "( 2.5.13.13 NAME 'booleanMatch' APPLIES ( hasSubordinates $ olcAddContentAcl $ olcGentleHUP $ olcHidden $ olcLastMod $ olcMirrorMode $ olcMonitoring $ olcReadOnly $ olcReverseLookup $ olcSyncUseSubentry $ olcDbNoSync $ olcMemberOfRefInt $ olcUniqueStrict ) )", - "( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )", - "( 2.5.13.9 NAME 'numericStringOrderingMatch' APPLIES ( x121Address $ internationaliSDNNumber ) )", - "( 2.5.13.8 NAME 'numericStringMatch' APPLIES ( x121Address $ internationaliSDNNumber ) )", - "( 2.5.13.7 NAME 'caseExactSubstringsMatch' APPLIES ( serialNumber $ c $ telephoneNumber $ destinationIndicator $ dnQualifier $ homePhone $ mobile $ pager ) )", - "( 2.5.13.6 NAME 'caseExactOrderingMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ olcMemberOfDangling $ olcMemberOfGroupOC $ olcMemberOfMemberAD $ olcMemberOfMemberOfAD $ olcMemberOfDanglingError $ olcUniqueIgnore $ olcUniqueAttribute $ olcUniqueURI $ olcRefintAttribute $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ ipHostNumber $ ipNetworkNumber $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", - "( 2.5.13.5 NAME 'caseExactMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ olcMemberOfDangling $ olcMemberOfGroupOC $ olcMemberOfMemberAD $ olcMemberOfMemberOfAD $ olcMemberOfDanglingError $ olcUniqueIgnore $ olcUniqueAttribute $ olcUniqueURI $ olcRefintAttribute $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ ipHostNumber $ ipNetworkNumber $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", - "( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' APPLIES ( serialNumber $ c $ telephoneNumber $ destinationIndicator $ dnQualifier $ homePhone $ mobile $ pager ) )", - "( 2.5.13.3 NAME 'caseIgnoreOrderingMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ olcMemberOfDangling $ olcMemberOfGroupOC $ olcMemberOfMemberAD $ olcMemberOfMemberOfAD $ olcMemberOfDanglingError $ olcUniqueIgnore $ olcUniqueAttribute $ olcUniqueURI $ olcRefintAttribute $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ ipHostNumber $ ipNetworkNumber $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", - "( 2.5.13.2 NAME 'caseIgnoreMatch' APPLIES ( supportedSASLMechanisms $ vendorName $ vendorVersion $ ref $ name $ cn $ uid $ labeledURI $ description $ olcConfigFile $ olcConfigDir $ olcAccess $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAttributeTypes $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcBackend $ olcDatabase $ olcDisallows $ olcDitContentRules $ olcExtraAttrs $ olcInclude $ olcLdapSyntaxes $ olcLimits $ olcLogFile $ olcLogLevel $ olcModuleLoad $ olcModulePath $ olcObjectClasses $ olcObjectIdentifier $ olcOverlay $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPlugin $ olcPluginLogFile $ olcReferral $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDSE $ olcRootPW $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSortVals $ olcSubordinate $ olcSyncrepl $ olcTCPBuffer $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSCRLFile $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSProtocolMin $ olcUpdateRef $ olcDbDirectory $ olcDbCheckpoint $ olcDbEnvFlags $ olcDbIndex $ olcDbMode $ olcMemberOfDangling $ olcMemberOfGroupOC $ olcMemberOfMemberAD $ olcMemberOfMemberOfAD $ olcMemberOfDanglingError $ olcUniqueIgnore $ olcUniqueAttribute $ olcUniqueURI $ olcRefintAttribute $ knowledgeInformation $ sn $ serialNumber $ c $ l $ st $ street $ o $ ou $ title $ businessCategory $ postalCode $ postOfficeBox $ physicalDeliveryOfficeName $ telephoneNumber $ destinationIndicator $ givenName $ initials $ generationQualifier $ dnQualifier $ houseIdentifier $ dmdName $ pseudonym $ textEncodedORAddress $ info $ drink $ roomNumber $ userClass $ host $ documentIdentifier $ documentTitle $ documentVersion $ documentLocation $ homePhone $ personalTitle $ mobile $ pager $ co $ uniqueIdentifier $ organizationalStatus $ buildingName $ documentPublisher $ ipServiceProtocol $ ipHostNumber $ ipNetworkNumber $ nisMapName $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ preferredLanguage ) )", - "( 2.5.13.1 NAME 'distinguishedNameMatch' APPLIES ( creatorsName $ modifiersName $ subschemaSubentry $ entryDN $ namingContexts $ aliasedObjectName $ dynamicSubtrees $ distinguishedName $ seeAlso $ olcDefaultSearchBase $ olcRootDN $ olcSchemaDN $ olcSuffix $ olcUpdateDN $ memberOf $ olcMemberOfDN $ olcUniqueBase $ olcRefintNothing $ olcRefintModifiersName $ member $ owner $ roleOccupant $ manager $ documentAuthor $ secretary $ associatedName $ dITRedirect ) )", - "( 2.5.13.0 NAME 'objectIdentifierMatch' APPLIES ( supportedControl $ supportedExtension $ supportedFeatures $ supportedApplicationContext ) )" - ], - "matchingRules": [ - "( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )", - "( 1.3.6.1.1.16.2 NAME 'UUIDMatch' SYNTAX 1.3.6.1.1.16.1 )", - "( 1.2.840.113556.1.4.804 NAME 'integerBitOrMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 1.2.840.113556.1.4.803 NAME 'integerBitAndMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 1.3.6.1.4.1.4203.1.2.1 NAME 'caseExactIA5SubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.4.1.1466.109.114.3 NAME 'caseIgnoreIA5SubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.4.1.1466.109.114.2 NAME 'caseIgnoreIA5Match' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 1.3.6.1.4.1.1466.109.114.1 NAME 'caseExactIA5Match' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )", - "( 2.5.13.38 NAME 'certificateListExactMatch' SYNTAX 1.3.6.1.1.15.5 )", - "( 2.5.13.34 NAME 'certificateExactMatch' SYNTAX 1.3.6.1.1.15.1 )", - "( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )", - "( 2.5.13.29 NAME 'integerFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )", - "( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )", - "( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )", - "( 2.5.13.21 NAME 'telephoneNumberSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.20 NAME 'telephoneNumberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )", - "( 2.5.13.19 NAME 'octetStringSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", - "( 2.5.13.18 NAME 'octetStringOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", - "( 2.5.13.17 NAME 'octetStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )", - "( 2.5.13.16 NAME 'bitStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", - "( 2.5.13.15 NAME 'integerOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 2.5.13.14 NAME 'integerMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )", - "( 2.5.13.13 NAME 'booleanMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )", - "( 2.5.13.11 NAME 'caseIgnoreListMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )", - "( 2.5.13.10 NAME 'numericStringSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.9 NAME 'numericStringOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", - "( 2.5.13.8 NAME 'numericStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )", - "( 2.5.13.7 NAME 'caseExactSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.6 NAME 'caseExactOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.5 NAME 'caseExactMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )", - "( 2.5.13.3 NAME 'caseIgnoreOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", - "( 1.2.36.79672281.1.13.3 NAME 'rdnMatch' SYNTAX 1.2.36.79672281.1.5.0 )", - "( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )", - "( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )" - ], - "modifyTimestamp": [ - "20200930024551Z" - ], - "nameForms": [], - "objectClass": [ - "top", - "subentry", - "subschema", - "extensibleObject" - ], - "objectClasses": [ - "( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )", - "( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC4512: extensible object' SUP top AUXILIARY )", - "( 2.5.6.1 NAME 'alias' DESC 'RFC4512: an alias' SUP top STRUCTURAL MUST aliasedObjectName )", - "( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref )", - "( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn )", - "( 2.5.17.0 NAME 'subentry' DESC 'RFC3672: subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) )", - "( 2.5.20.1 NAME 'subschema' DESC 'RFC4512: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ dITContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )", - "( 1.3.6.1.4.1.1466.101.119.2 NAME 'dynamicObject' DESC 'RFC2589: Dynamic Object' SUP top AUXILIARY )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.0 NAME 'olcConfig' DESC 'OpenLDAP configuration object' SUP top ABSTRACT )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.1 NAME 'olcGlobal' DESC 'OpenLDAP Global configuration options' SUP olcConfig STRUCTURAL MAY ( cn $ olcConfigFile $ olcConfigDir $ olcAllows $ olcArgsFile $ olcAttributeOptions $ olcAuthIDRewrite $ olcAuthzPolicy $ olcAuthzRegexp $ olcConcurrency $ olcConnMaxPending $ olcConnMaxPendingAuth $ olcDisallows $ olcGentleHUP $ olcIdleTimeout $ olcIndexSubstrIfMaxLen $ olcIndexSubstrIfMinLen $ olcIndexSubstrAnyLen $ olcIndexSubstrAnyStep $ olcIndexIntLen $ olcListenerThreads $ olcLocalSSF $ olcLogFile $ olcLogLevel $ olcPasswordCryptSaltFormat $ olcPasswordHash $ olcPidFile $ olcPluginLogFile $ olcReadOnly $ olcReferral $ olcReplogFile $ olcRequires $ olcRestrict $ olcReverseLookup $ olcRootDSE $ olcSaslAuxprops $ olcSaslHost $ olcSaslRealm $ olcSaslSecProps $ olcSecurity $ olcServerID $ olcSizeLimit $ olcSockbufMaxIncoming $ olcSockbufMaxIncomingAuth $ olcTCPBuffer $ olcThreads $ olcTimeLimit $ olcTLSCACertificateFile $ olcTLSCACertificatePath $ olcTLSCertificateFile $ olcTLSCertificateKeyFile $ olcTLSCipherSuite $ olcTLSCRLCheck $ olcTLSRandFile $ olcTLSVerifyClient $ olcTLSDHParamFile $ olcTLSCRLFile $ olcTLSProtocolMin $ olcToolThreads $ olcWriteTimeout $ olcObjectIdentifier $ olcAttributeTypes $ olcObjectClasses $ olcDitContentRules $ olcLdapSyntaxes ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.2 NAME 'olcSchemaConfig' DESC 'OpenLDAP schema object' SUP olcConfig STRUCTURAL MAY ( cn $ olcObjectIdentifier $ olcLdapSyntaxes $ olcAttributeTypes $ olcObjectClasses $ olcDitContentRules ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.3 NAME 'olcBackendConfig' DESC 'OpenLDAP Backend-specific options' SUP olcConfig STRUCTURAL MUST olcBackend )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.4 NAME 'olcDatabaseConfig' DESC 'OpenLDAP Database-specific options' SUP olcConfig STRUCTURAL MUST olcDatabase MAY ( olcHidden $ olcSuffix $ olcSubordinate $ olcAccess $ olcAddContentAcl $ olcLastMod $ olcLimits $ olcMaxDerefDepth $ olcPlugin $ olcReadOnly $ olcReplica $ olcReplicaArgsFile $ olcReplicaPidFile $ olcReplicationInterval $ olcReplogFile $ olcRequires $ olcRestrict $ olcRootDN $ olcRootPW $ olcSchemaDN $ olcSecurity $ olcSizeLimit $ olcSyncUseSubentry $ olcSyncrepl $ olcTimeLimit $ olcUpdateDN $ olcUpdateRef $ olcMirrorMode $ olcMonitoring $ olcExtraAttrs ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.5 NAME 'olcOverlayConfig' DESC 'OpenLDAP Overlay-specific options' SUP olcConfig STRUCTURAL MUST olcOverlay )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.6 NAME 'olcIncludeFile' DESC 'OpenLDAP configuration include file' SUP olcConfig STRUCTURAL MUST olcInclude MAY ( cn $ olcRootDSE ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.7 NAME 'olcFrontendConfig' DESC 'OpenLDAP frontend configuration' AUXILIARY MAY ( olcDefaultSearchBase $ olcPasswordHash $ olcSortVals ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.0.8 NAME 'olcModuleList' DESC 'OpenLDAP dynamic module info' SUP olcConfig STRUCTURAL MAY ( cn $ olcModulePath $ olcModuleLoad ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.2.2.1 NAME 'olcLdifConfig' DESC 'LDIF backend configuration' SUP olcDatabaseConfig STRUCTURAL MUST olcDbDirectory )", - "( 1.3.6.1.4.1.4203.1.12.2.4.2.12.1 NAME 'olcMdbConfig' DESC 'MDB backend configuration' SUP olcDatabaseConfig STRUCTURAL MUST olcDbDirectory MAY ( olcDbCheckpoint $ olcDbEnvFlags $ olcDbNoSync $ olcDbIndex $ olcDbMaxReaders $ olcDbMaxSize $ olcDbMode $ olcDbSearchStack $ olcDbRtxnSize ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.3.18.1 NAME 'olcMemberOf' DESC 'Member-of configuration' SUP olcOverlayConfig STRUCTURAL MAY ( olcMemberOfDN $ olcMemberOfDangling $ olcMemberOfDanglingError $ olcMemberOfRefInt $ olcMemberOfGroupOC $ olcMemberOfMemberAD $ olcMemberOfMemberOfAD ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.3.10.1 NAME 'olcUniqueConfig' DESC 'Attribute value uniqueness configuration' SUP olcOverlayConfig STRUCTURAL MAY ( olcUniqueBase $ olcUniqueIgnore $ olcUniqueAttribute $ olcUniqueStrict $ olcUniqueURI ) )", - "( 1.3.6.1.4.1.4203.1.12.2.4.3.11.1 NAME 'olcRefintConfig' DESC 'Referential integrity configuration' SUP olcOverlayConfig STRUCTURAL MAY ( olcRefintAttribute $ olcRefintNothing $ olcRefintModifiersName ) )", - "( 2.5.6.2 NAME 'country' DESC 'RFC2256: a country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) )", - "( 2.5.6.3 NAME 'locality' DESC 'RFC2256: a locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) )", - "( 2.5.6.4 NAME 'organization' DESC 'RFC2256: an organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", - "( 2.5.6.5 NAME 'organizationalUnit' DESC 'RFC2256: an organizational unit' SUP top STRUCTURAL MUST ou MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", - "( 2.5.6.6 NAME 'person' DESC 'RFC2256: a person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )", - "( 2.5.6.7 NAME 'organizationalPerson' DESC 'RFC2256: an organizational person' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )", - "( 2.5.6.8 NAME 'organizationalRole' DESC 'RFC2256: an organizational role' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) )", - "( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256: a group of names (DNs)' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )", - "( 2.5.6.10 NAME 'residentialPerson' DESC 'RFC2256: an residential person' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) )", - "( 2.5.6.11 NAME 'applicationProcess' DESC 'RFC2256: an application process' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) )", - "( 2.5.6.12 NAME 'applicationEntity' DESC 'RFC2256: an application entity' SUP top STRUCTURAL MUST ( presentationAddress $ cn ) MAY ( supportedApplicationContext $ seeAlso $ ou $ o $ l $ description ) )", - "( 2.5.6.13 NAME 'dSA' DESC 'RFC2256: a directory system agent (a server)' SUP applicationEntity STRUCTURAL MAY knowledgeInformation )", - "( 2.5.6.14 NAME 'device' DESC 'RFC2256: a device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) )", - "( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'RFC2256: a strong authentication user' SUP top AUXILIARY MUST userCertificate )", - "( 2.5.6.16 NAME 'certificationAuthority' DESC 'RFC2256: a certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair )", - "( 2.5.6.17 NAME 'groupOfUniqueNames' DESC 'RFC2256: a group of unique names (DN and Unique Identifier)' SUP top STRUCTURAL MUST ( uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )", - "( 2.5.6.18 NAME 'userSecurityInformation' DESC 'RFC2256: a user security information' SUP top AUXILIARY MAY supportedAlgorithms )", - "( 2.5.6.16.2 NAME 'certificationAuthority-V2' SUP certificationAuthority AUXILIARY MAY deltaRevocationList )", - "( 2.5.6.19 NAME 'cRLDistributionPoint' SUP top STRUCTURAL MUST cn MAY ( certificateRevocationList $ authorityRevocationList $ deltaRevocationList ) )", - "( 2.5.6.20 NAME 'dmd' SUP top STRUCTURAL MUST dmdName MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )", - "( 2.5.6.21 NAME 'pkiUser' DESC 'RFC2587: a PKI user' SUP top AUXILIARY MAY userCertificate )", - "( 2.5.6.22 NAME 'pkiCA' DESC 'RFC2587: PKI certificate authority' SUP top AUXILIARY MAY ( authorityRevocationList $ certificateRevocationList $ cACertificate $ crossCertificatePair ) )", - "( 2.5.6.23 NAME 'deltaCRL' DESC 'RFC2587: PKI user' SUP top AUXILIARY MAY deltaRevocationList )", - "( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'RFC2079: object that contains the URI attribute type' SUP top AUXILIARY MAY labeledURI )", - "( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' DESC 'RFC1274: simple security object' SUP top AUXILIARY MUST userPassword )", - "( 1.3.6.1.4.1.1466.344 NAME 'dcObject' DESC 'RFC2247: domain component object' SUP top AUXILIARY MUST dc )", - "( 1.3.6.1.1.3.1 NAME 'uidObject' DESC 'RFC2377: uid object' SUP top AUXILIARY MUST uid )", - "( 0.9.2342.19200300.100.4.4 NAME ( 'pilotPerson' 'newPilotPerson' ) SUP person STRUCTURAL MAY ( userid $ textEncodedORAddress $ rfc822Mailbox $ favouriteDrink $ roomNumber $ userClass $ homeTelephoneNumber $ homePostalAddress $ secretary $ personalTitle $ preferredDeliveryMethod $ businessCategory $ janetMailbox $ otherMailbox $ mobileTelephoneNumber $ pagerTelephoneNumber $ organizationalStatus $ mailPreferenceOption $ personalSignature ) )", - "( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST userid MAY ( description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ host ) )", - "( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( commonName $ description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) )", - "( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST commonName MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) )", - "( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST commonName MAY ( description $ seeAlso $ telephonenumber $ localityName $ organizationName $ organizationalUnitName ) )", - "( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST domainComponent MAY ( associatedName $ organizationName $ description $ businessCategory $ seeAlso $ searchGuide $ userPassword $ localityName $ stateOrProvinceName $ streetAddress $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) )", - "( 0.9.2342.19200300.100.4.14 NAME 'RFC822localPart' SUP domain STRUCTURAL MAY ( commonName $ surname $ description $ seeAlso $ telephoneNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) )", - "( 0.9.2342.19200300.100.4.15 NAME 'dNSDomain' SUP domain STRUCTURAL MAY ( ARecord $ MDRecord $ MXRecord $ NSRecord $ SOARecord $ CNAMERecord ) )", - "( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' DESC 'RFC1274: an object related to an domain' SUP top AUXILIARY MUST associatedDomain )", - "( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST friendlyCountryName )", - "( 0.9.2342.19200300.100.4.20 NAME 'pilotOrganization' SUP ( organization $ organizationalUnit ) STRUCTURAL MAY buildingName )", - "( 0.9.2342.19200300.100.4.21 NAME 'pilotDSA' SUP dsa STRUCTURAL MAY dSAQuality )", - "( 0.9.2342.19200300.100.4.22 NAME 'qualityLabelledData' SUP top AUXILIARY MUST dsaQuality MAY ( subtreeMinimumQuality $ subtreeMaximumQuality ) )", - "( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) )", - "( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' DESC 'Additional attributes for shadow passwords' SUP top AUXILIARY MUST uid MAY ( userPassword $ description $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag ) )", - "( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top AUXILIARY MUST gidNumber MAY ( userPassword $ memberUid $ description ) )", - "( 1.3.6.1.1.1.2.3 NAME 'ipService' DESC 'Abstraction an Internet Protocol service. Maps an IP port and protocol (such as tcp or udp) to one or more names; the distinguished value of the cn attribute denotes the services canonical name' SUP top STRUCTURAL MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY description )", - "( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' DESC 'Abstraction of an IP protocol. Maps a protocol number to one or more names. The distinguished value of the cn attribute denotes the protocols canonical name' SUP top STRUCTURAL MUST ( cn $ ipProtocolNumber ) MAY description )", - "( 1.3.6.1.1.1.2.5 NAME 'oncRpc' DESC 'Abstraction of an Open Network Computing (ONC) [RFC1057] Remote Procedure Call (RPC) binding. This class maps an ONC RPC number to a name. The distinguished value of the cn attribute denotes the RPC services canonical name' SUP top STRUCTURAL MUST ( cn $ oncRpcNumber ) MAY description )", - "( 1.3.6.1.1.1.2.6 NAME 'ipHost' DESC 'Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the hosts canonical name. Device SHOULD be used as a structural class' SUP top AUXILIARY MUST ( cn $ ipHostNumber ) MAY ( userPassword $ l $ description $ manager ) )", - "( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' DESC 'Abstraction of a network. The distinguished value of the cn attribute denotes the networks canonical name' SUP top STRUCTURAL MUST ipNetworkNumber MAY ( cn $ ipNetmaskNumber $ l $ description $ manager ) )", - "( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' DESC 'Abstraction of a netgroup. May refer to other netgroups' SUP top STRUCTURAL MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )", - "( 1.3.6.1.1.1.2.9 NAME 'nisMap' DESC 'A generic abstraction of a NIS map' SUP top STRUCTURAL MUST nisMapName MAY description )", - "( 1.3.6.1.1.1.2.10 NAME 'nisObject' DESC 'An entry in a NIS map' SUP top STRUCTURAL MUST ( cn $ nisMapEntry $ nisMapName ) MAY description )", - "( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' DESC 'A device with a MAC address; device SHOULD be used as a structural class' SUP top AUXILIARY MAY macAddress )", - "( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' DESC 'A device with boot parameters; device SHOULD be used as a structural class' SUP top AUXILIARY MAY ( bootFile $ bootParameter ) )", - "( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject' DESC 'An object with a public and secret key' SUP top AUXILIARY MUST ( cn $ nisPublicKey $ nisSecretKey ) MAY ( uidNumber $ description ) )", - "( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject' DESC 'Associates a NIS domain with a naming context' SUP top AUXILIARY MUST nisDomain )", - "( 1.3.6.1.1.1.2.16 NAME 'automountMap' SUP top STRUCTURAL MUST automountMapName MAY description )", - "( 1.3.6.1.1.1.2.17 NAME 'automount' DESC 'Automount information' SUP top STRUCTURAL MUST ( automountKey $ automountInformation ) MAY description )", - "( 1.3.6.1.4.1.5322.13.1.1 NAME 'namedObject' SUP top STRUCTURAL MAY cn )", - "( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RFC2798: Internet Organizational Person' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) )", - "( 1.3.6.1.4.1.4203.666.1.100 NAME 'postfixVirtual' DESC 'Postfix virtual map class' SUP top STRUCTURAL MUST uid MAY ( mailacceptinggeneralid $ maildrop ) )" - ], - "structuralObjectClass": [ - "subentry" - ], - "subschemaSubentry": [ - "cn=Subschema" - ] - }, - "schema_entry": "cn=Subschema", - "type": "SchemaInfo" -} \ No newline at end of file diff --git a/tests/test_invite.py b/tests/test_invite.py index 94f7967d..9a7304ee 100644 --- a/tests/test_invite.py +++ b/tests/test_invite.py @@ -6,7 +6,6 @@ from flask import url_for, session, current_app # These imports are required, because otherwise we get circular imports?! from uffd import user -from uffd.ldap import ldap from uffd import create_app, db from uffd.invite.models import Invite, InviteGrant, InviteSignup @@ -18,7 +17,7 @@ from utils import dump, UffdTestCase, db_flush class TestInviteModel(UffdTestCase): def test_expire(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertFalse(invite.expired) self.assertTrue(invite.active) invite.valid_until = datetime.datetime.now() - datetime.timedelta(seconds=60) @@ -26,13 +25,13 @@ class TestInviteModel(UffdTestCase): self.assertFalse(invite.active) def test_void(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=False) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=False, creator=self.get_admin()) self.assertFalse(invite.voided) self.assertTrue(invite.active) invite.used = True self.assertFalse(invite.voided) self.assertTrue(invite.active) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=True, creator=self.get_admin()) self.assertFalse(invite.voided) self.assertTrue(invite.active) invite.used = True @@ -42,50 +41,47 @@ class TestInviteModel(UffdTestCase): def test_permitted(self): role = Role(name='testrole') invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, roles=[role]) - self.assertTrue(invite.permitted) - self.assertTrue(invite.active) - invite.creator_dn = 'uid=doesnotexist,ou=users,dc=example,dc=com' self.assertFalse(invite.permitted) self.assertFalse(invite.active) - invite.creator_dn = self.test_data.get('admin').get('dn') + invite.creator = self.get_admin() self.assertTrue(invite.permitted) self.assertTrue(invite.active) - invite.creator_dn = self.test_data.get('user').get('dn') + invite.creator = self.get_user() self.assertFalse(invite.permitted) self.assertFalse(invite.active) - role.moderator_group_dn = self.test_data.get('group_uffd_access').get('dn') + role.moderator_group = self.get_access_group() current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' self.assertTrue(invite.permitted) self.assertTrue(invite.active) - role.moderator_group_dn = None + role.moderator_group = None self.assertFalse(invite.permitted) self.assertFalse(invite.active) - role.moderator_group_dn = self.test_data.get('group_uffd_access').get('dn') + role.moderator_group = self.get_access_group() current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_admin' self.assertFalse(invite.permitted) self.assertFalse(invite.active) def test_disable(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertTrue(invite.active) invite.disable() self.assertFalse(invite.active) def test_reset_disabled(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) invite.disable() self.assertFalse(invite.active) invite.reset() self.assertTrue(invite.active) def test_reset_expired(self): - invite = Invite(valid_until=datetime.datetime.now() - datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.now() - datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertFalse(invite.active) invite.reset() self.assertFalse(invite.active) def test_reset_single_use(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=False) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), single_use=False, creator=self.get_admin()) invite.used = True invite.disable() self.assertFalse(invite.active) @@ -93,7 +89,7 @@ class TestInviteModel(UffdTestCase): self.assertTrue(invite.active) def test_short_token(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) db.session.add(invite) db.session.commit() self.assertTrue(len(invite.short_token) <= len(invite.token)/3) @@ -104,14 +100,14 @@ class TestInviteGrantModel(UffdTestCase): group0 = self.get_access_group() role0 = Role(name='baserole', groups={group0: RoleGroup(group=group0)}) db.session.add(role0) - user.roles.add(role0) + user.roles.append(role0) user.update_groups() group1 = self.get_admin_group() role1 = Role(name='testrole1', groups={group1: RoleGroup(group=group1)}) db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2]) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], creator=self.get_admin()) self.assertIn(role0, user.roles) self.assertNotIn(role1, user.roles) self.assertNotIn(role2, user.roles) @@ -128,21 +124,20 @@ class TestInviteGrantModel(UffdTestCase): self.assertIn(group1, user.groups) self.assertTrue(invite.used) db.session.commit() - ldap.session.commit() db_flush() user = self.get_user() self.assertIn('baserole', [role.name for role in user.roles_effective]) self.assertIn('testrole1', [role.name for role in user.roles]) self.assertIn('testrole2', [role.name for role in user.roles]) - self.assertIn(self.test_data.get('group_uffd_access').get('dn'), [group.dn for group in user.groups]) - self.assertIn(self.test_data.get('group_uffd_admin').get('dn'), [group.dn for group in user.groups]) + self.assertIn(self.get_access_group(), user.groups) + self.assertIn(self.get_admin_group(), user.groups) def test_inactive(self): user = self.get_user() group = self.get_admin_group() role = Role(name='testrole1', groups={group: RoleGroup(group=group)}) db.session.add(role) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role], single_use=True, used=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role], single_use=True, used=True, creator=self.get_admin()) self.assertFalse(invite.active) grant = InviteGrant(invite=invite, user=user) success, msg = grant.apply() @@ -153,7 +148,7 @@ class TestInviteGrantModel(UffdTestCase): def test_no_roles(self): user = self.get_user() - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60)) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), creator=self.get_admin()) self.assertTrue(invite.active) grant = InviteGrant(invite=invite, user=user) success, msg = grant.apply() @@ -164,8 +159,8 @@ class TestInviteGrantModel(UffdTestCase): user = self.get_user() role = Role(name='testrole1') db.session.add(role) - user.roles.add(role) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role]) + user.roles.append(role) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role], creator=self.get_admin()) self.assertTrue(invite.active) grant = InviteGrant(invite=invite, user=user) success, msg = grant.apply() @@ -190,7 +185,7 @@ class TestInviteSignupModel(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True, creator=self.get_admin()) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') self.assertFalse(invite.used) valid, msg = signup.validate() @@ -202,7 +197,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertEqual(user.loginname, 'newuser') self.assertEqual(user.displayname, 'New User') self.assertEqual(user.mail, 'test@example.com') - self.assertEqual(signup.user.dn, user.dn) + self.assertEqual(signup.user, user) self.assertIn(base_role, user.roles_effective) self.assertIn(role1, user.roles) self.assertIn(role2, user.roles) @@ -210,7 +205,6 @@ class TestInviteSignupModel(UffdTestCase): self.assertIn(base_group2, user.groups) self.assertIn(group, user.groups) db.session.commit() - ldap.session.commit() db_flush() self.assertEqual(len(User.query.filter_by(loginname='newuser').all()), 1) @@ -219,7 +213,7 @@ class TestInviteSignupModel(UffdTestCase): base_role = Role.query.filter_by(name='base').one() base_group1 = self.get_access_group() base_group2 = self.get_users_group() - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') self.assertFalse(invite.used) valid, msg = signup.validate() @@ -231,19 +225,18 @@ class TestInviteSignupModel(UffdTestCase): self.assertEqual(user.loginname, 'newuser') self.assertEqual(user.displayname, 'New User') self.assertEqual(user.mail, 'test@example.com') - self.assertEqual(signup.user.dn, user.dn) + self.assertEqual(signup.user, user) self.assertIn(base_role, user.roles_effective) self.assertEqual(len(user.roles_effective), 1) self.assertIn(base_group1, user.groups) self.assertIn(base_group2, user.groups) self.assertEqual(len(user.groups), 2) db.session.commit() - ldap.session.commit() db_flush() self.assertEqual(len(User.query.filter_by(loginname='newuser').all()), 1) def test_inactive(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, single_use=True, used=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, single_use=True, used=True, creator=self.get_admin()) self.assertFalse(invite.active) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') valid, msg = signup.validate() @@ -254,7 +247,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertIsInstance(msg, str) def test_invalid(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) self.assertTrue(invite.active) signup = InviteSignup(invite=invite, loginname='', displayname='New User', mail='test@example.com', password='notsecret') valid, msg = signup.validate() @@ -262,7 +255,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertIsInstance(msg, str) def test_invalid2(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) self.assertTrue(invite.active) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') user, msg = signup.finish('wrongpassword') @@ -270,7 +263,7 @@ class TestInviteSignupModel(UffdTestCase): self.assertIsInstance(msg, str) def test_no_signup(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) self.assertTrue(invite.active) signup = InviteSignup(invite=invite, loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') valid, msg = signup.validate() @@ -295,16 +288,18 @@ class TestInviteAdminViews(UffdTestCase): role2 = Role(name='testrole2') db.session.add(role2) # All possible states - db.session.add(Invite(valid_until=valid_until, single_use=False)) - db.session.add(Invite(valid_until=valid_until, single_use=True, used=False)) - db.session.add(Invite(valid_until=valid_until, single_use=True, used=True, signups=[InviteSignup(user=user1)])) - db.session.add(Invite(valid_until=valid_until_expired)) - db.session.add(Invite(valid_until=valid_until, disabled=True)) + db.session.add(Invite(valid_until=valid_until, single_use=False, creator=self.get_admin())) + db.session.add(Invite(valid_until=valid_until, single_use=True, used=False, creator=self.get_admin())) + invite = Invite(valid_until=valid_until, single_use=True, used=True, creator=self.get_admin()) + invite.signups = [InviteSignup(user=user1)] + db.session.add(invite) + db.session.add(Invite(valid_until=valid_until_expired, creator=self.get_admin())) + db.session.add(Invite(valid_until=valid_until, disabled=True, creator=self.get_admin())) # Different permissions - db.session.add(Invite(valid_until=valid_until, allow_signup=True)) - db.session.add(Invite(valid_until=valid_until, allow_signup=False)) - db.session.add(Invite(valid_until=valid_until, allow_signup=True, roles=[role1], grants=[InviteGrant(user=user2)])) - db.session.add(Invite(valid_until=valid_until, allow_signup=False, roles=[role1, role2])) + db.session.add(Invite(valid_until=valid_until, allow_signup=True, creator=self.get_admin())) + db.session.add(Invite(valid_until=valid_until, allow_signup=False, creator=self.get_admin())) + db.session.add(Invite(valid_until=valid_until, allow_signup=True, roles=[role1], grants=[InviteGrant(user=user2)], creator=self.get_admin())) + db.session.add(Invite(valid_until=valid_until, allow_signup=False, roles=[role1, role2], creator=self.get_admin())) db.session.commit() self.login_as('admin') r = self.client.get(path=url_for('invite.index'), follow_redirects=True) @@ -331,9 +326,9 @@ class TestInviteAdminViews(UffdTestCase): def test_index_signupperm(self): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - invite1 = Invite(valid_until=valid_until, allow_signup=True, creator_dn=self.test_data.get('admin').get('dn')) + invite1 = Invite(valid_until=valid_until, allow_signup=True, creator=self.get_admin()) db.session.add(invite1) - invite2 = Invite(valid_until=valid_until, allow_signup=True, creator_dn=self.test_data.get('user').get('dn')) + invite2 = Invite(valid_until=valid_until, allow_signup=True, creator=self.get_user()) db.session.add(invite2) invite3 = Invite(valid_until=valid_until, allow_signup=True) db.session.add(invite3) @@ -351,7 +346,7 @@ class TestInviteAdminViews(UffdTestCase): def test_index_rolemod(self): role1 = Role(name='testrole1') db.session.add(role1) - role2 = Role(name='testrole2', moderator_group_dn=self.test_data.get('group_uffd_access').get('dn')) + role2 = Role(name='testrole2', moderator_group=self.get_access_group()) db.session.add(role2) valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) db.session.add(Invite(valid_until=valid_until, roles=[role1])) @@ -412,7 +407,7 @@ class TestInviteAdminViews(UffdTestCase): def test_disable(self): self.login_as('admin') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - invite = Invite(valid_until=valid_until) + invite = Invite(valid_until=valid_until, creator=self.get_admin()) db.session.add(invite) db.session.commit() id = invite.id @@ -425,7 +420,7 @@ class TestInviteAdminViews(UffdTestCase): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' self.login_as('user') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - invite = Invite(valid_until=valid_until, creator_dn=self.test_data.get('user').get('dn')) + invite = Invite(valid_until=valid_until, creator=self.get_user()) db.session.add(invite) db.session.commit() id = invite.id @@ -437,9 +432,9 @@ class TestInviteAdminViews(UffdTestCase): def test_disable_rolemod(self): self.login_as('user') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - role = Role(name='testrole', moderator_group_dn=self.test_data.get('group_uffd_access').get('dn')) + role = Role(name='testrole', moderator_group=self.get_access_group()) db.session.add(role) - invite = Invite(valid_until=valid_until, creator_dn=self.test_data.get('admin').get('dn'), roles=[role]) + invite = Invite(valid_until=valid_until, creator=self.get_admin(), roles=[role]) db.session.add(invite) db.session.commit() id = invite.id @@ -450,10 +445,10 @@ class TestInviteAdminViews(UffdTestCase): def test_disable_noperm(self): self.login_as('user') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - db.session.add(Role(name='testrole1', moderator_group_dn=self.test_data.get('group_uffd_access').get('dn'))) - role = Role(name='testrole2', moderator_group_dn=self.test_data.get('group_uffd_admin').get('dn')) + db.session.add(Role(name='testrole1', moderator_group=self.get_access_group())) + role = Role(name='testrole2', moderator_group=self.get_admin_group()) db.session.add(role) - invite = Invite(valid_until=valid_until, creator_dn=self.test_data.get('admin').get('dn'), roles=[role]) + invite = Invite(valid_until=valid_until, creator=self.get_admin(), roles=[role]) db.session.add(invite) db.session.commit() id = invite.id @@ -464,7 +459,7 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_disabled(self): self.login_as('admin') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - invite = Invite(valid_until=valid_until, disabled=True) + invite = Invite(valid_until=valid_until, disabled=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() id = invite.id @@ -476,7 +471,7 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_voided(self): self.login_as('admin') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - invite = Invite(valid_until=valid_until, single_use=True, used=True) + invite = Invite(valid_until=valid_until, single_use=True, used=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() id = invite.id @@ -489,7 +484,7 @@ class TestInviteAdminViews(UffdTestCase): current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access' self.login_as('user') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - invite = Invite(valid_until=valid_until, disabled=True, creator_dn=self.test_data.get('user').get('dn')) + invite = Invite(valid_until=valid_until, disabled=True, creator=self.get_user()) db.session.add(invite) db.session.commit() id = invite.id @@ -501,9 +496,9 @@ class TestInviteAdminViews(UffdTestCase): def test_reset_foreign(self): self.login_as('user') valid_until = datetime.datetime.now() + datetime.timedelta(seconds=60) - role = Role(name='testrole', moderator_group_dn=self.test_data.get('group_uffd_access').get('dn')) + role = Role(name='testrole', moderator_group=self.get_access_group()) db.session.add(role) - invite = Invite(valid_until=valid_until, disabled=True, creator_dn=self.test_data.get('admin').get('dn'), roles=[role]) + invite = Invite(valid_until=valid_until, disabled=True, creator=self.get_admin(), roles=[role]) db.session.add(invite) db.session.commit() id = invite.id @@ -551,17 +546,16 @@ class TestInviteUseViews(UffdTestCase): group0 = self.get_access_group() role0 = Role(name='baserole', groups={group0: RoleGroup(group=group0)}) db.session.add(role0) - user.roles.add(role0) + user.roles.append(role0) user.update_groups() group1 = self.get_admin_group() role1 = Role(name='testrole1', groups={group1: RoleGroup(group=group1)}) db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2]) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], creator=self.get_admin()) db.session.add(invite) db.session.commit() - ldap.session.commit() invite_id = invite.id token = invite.token self.assertIn(role0, user.roles) @@ -581,8 +575,8 @@ class TestInviteUseViews(UffdTestCase): self.assertIn('baserole', [role.name for role in user.roles]) self.assertIn('testrole1', [role.name for role in user.roles]) self.assertIn('testrole2', [role.name for role in user.roles]) - self.assertIn(self.test_data.get('group_uffd_access').get('dn'), [group.dn for group in user.groups]) - self.assertIn(self.test_data.get('group_uffd_admin').get('dn'), [group.dn for group in user.groups]) + self.assertIn(self.get_access_group(), user.groups) + self.assertIn(self.get_admin_group(), user.groups) def test_grant_invalid_invite(self): invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), disabled=True) @@ -612,7 +606,7 @@ class TestInviteUseViews(UffdTestCase): user = self.get_user() role = Role(name='testrole') db.session.add(role) - user.roles.add(role) + user.roles.append(role) invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role]) db.session.add(invite) db.session.commit() @@ -634,7 +628,7 @@ class TestInviteUseViews(UffdTestCase): db.session.add(role1) role2 = Role(name='testrole2') db.session.add(role2) - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), roles=[role1, role2], allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -656,7 +650,7 @@ class TestInviteUseViews(UffdTestCase): self.assertTrue(signup.validate()[0]) def test_signup_invalid_invite(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.get(path=url_for('invite.signup_start', invite_id=invite.id, token=invite.token), follow_redirects=True) @@ -664,7 +658,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_nosignup(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.get(path=url_for('invite.signup_start', invite_id=invite.id, token=invite.token), follow_redirects=True) @@ -672,7 +666,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_wrongpassword(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token), @@ -682,7 +676,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_invalid(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token), @@ -693,7 +687,7 @@ class TestInviteUseViews(UffdTestCase): def test_signup_mailerror(self): self.app.config['MAIL_SKIP_SEND'] = 'fail' - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() r = self.client.post(path=url_for('invite.signup_submit', invite_id=invite.id, token=invite.token), @@ -703,7 +697,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_signup_hostlimit(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -723,7 +717,7 @@ class TestInviteUseViews(UffdTestCase): self.assertIsNone(self.app.last_mail) def test_signup_mailimit(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -743,7 +737,7 @@ class TestInviteUseViews(UffdTestCase): self.assertIsNone(self.app.last_mail) def test_signup_check(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -755,19 +749,20 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'ok') def test_signup_check_invalid(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id token = invite.token r = self.client.post(path=url_for('invite.signup_check', invite_id=invite_id, token=token), follow_redirects=True, data={'loginname': ''}) + print(r.data) self.assertEqual(r.status_code, 200) self.assertEqual(r.content_type, 'application/json') self.assertEqual(r.json['status'], 'invalid') def test_signup_check_exists(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -779,7 +774,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'exists') def test_signup_check_nosignup(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=False, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -791,7 +786,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'error') def test_signup_check_error(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, disabled=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -803,7 +798,7 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.json['status'], 'error') def test_signup_check_ratelimited(self): - invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True) + invite = Invite(valid_until=datetime.datetime.now() + datetime.timedelta(seconds=60), allow_signup=True, creator=self.get_admin()) db.session.add(invite) db.session.commit() invite_id = invite.id @@ -818,6 +813,3 @@ class TestInviteUseViews(UffdTestCase): self.assertEqual(r.status_code, 200) self.assertEqual(r.content_type, 'application/json') self.assertEqual(r.json['status'], 'ratelimited') - -class TestInviteUseViewsOL(TestInviteUseViews): - use_openldap = True diff --git a/tests/test_mail.py b/tests/test_mail.py index 80c49e36..0ea10982 100644 --- a/tests/test_mail.py +++ b/tests/test_mail.py @@ -5,16 +5,12 @@ import unittest from flask import url_for, session # These imports are required, because otherwise we get circular imports?! -from uffd.ldap import ldap from uffd import user from uffd.mail.models import Mail from uffd import create_app, db -from utils import dump, UffdTestCase - -def get_mail(): - return Mail.query.get('uid=test,ou=postfix,dc=example,dc=com') +from utils import dump, UffdTestCase, db_flush class TestMailViews(UffdTestCase): def setUp(self): @@ -27,15 +23,15 @@ class TestMailViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_index_empty(self): - ldap.session.delete(get_mail()) - ldap.session.commit() - self.assertIsNone(get_mail()) + db.session.delete(self.get_mail()) + db.session.commit() + self.assertIsNone(self.get_mail()) r = self.client.get(path=url_for('mail.index'), follow_redirects=True) dump('mail_index_empty', r) self.assertEqual(r.status_code, 200) def test_show(self): - r = self.client.get(path=url_for('mail.show', uid=get_mail().uid), follow_redirects=True) + r = self.client.get(path=url_for('mail.show', uid=self.get_mail().uid), follow_redirects=True) dump('mail_show', r) self.assertEqual(r.status_code, 200) @@ -45,7 +41,7 @@ class TestMailViews(UffdTestCase): self.assertEqual(r.status_code, 200) def test_update(self): - m = get_mail() + m = self.get_mail() self.assertIsNotNone(m) self.assertEqual(m.uid, 'test') self.assertEqual(sorted(m.receivers), ['test1@example.com', 'test2@example.com']) @@ -55,7 +51,7 @@ class TestMailViews(UffdTestCase): 'mail-destinations': 'testuser@mail.example.com\ntestadmin@mail.example.com'}, follow_redirects=True) dump('mail_update', r) self.assertEqual(r.status_code, 200) - m = get_mail() + m = self.get_mail() self.assertIsNotNone(m) self.assertEqual(m.uid, 'test') self.assertEqual(sorted(m.receivers), ['foo@bar.com', 'test@bar.com']) @@ -67,7 +63,7 @@ class TestMailViews(UffdTestCase): 'mail-destinations': 'testuser@mail.example.com\ntestadmin@mail.example.com'}, follow_redirects=True) dump('mail_create', r) self.assertEqual(r.status_code, 200) - m = Mail.query.get('uid=test1,ou=postfix,dc=example,dc=com') + m = Mail.query.filter_by(uid='test1').one() self.assertEqual(m.uid, 'test1') self.assertEqual(sorted(m.receivers), ['foo@bar.com', 'test@bar.com']) self.assertEqual(sorted(m.destinations), ['testadmin@mail.example.com', 'testuser@mail.example.com']) @@ -79,18 +75,15 @@ class TestMailViews(UffdTestCase): 'mail-destinations': 'testuser@mail.example.com\ntestadmin@mail.example.com'}, follow_redirects=True) dump('mail_create_error', r) self.assertEqual(r.status_code, 200) - m = get_mail() + m = self.get_mail() self.assertIsNotNone(m) self.assertEqual(m.uid, 'test') self.assertEqual(sorted(m.receivers), ['test1@example.com', 'test2@example.com']) self.assertEqual(sorted(m.destinations), ['testuser@mail.example.com']) def test_delete(self): - self.assertIsNotNone(get_mail()) - r = self.client.get(path=url_for('mail.delete', uid=get_mail().uid), follow_redirects=True) + self.assertIsNotNone(self.get_mail()) + r = self.client.get(path=url_for('mail.delete', uid=self.get_mail().uid), follow_redirects=True) dump('mail_delete', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(get_mail()) - -class TestMailViewsOL(TestMailViews): - use_openldap = True + self.assertIsNone(self.get_mail()) diff --git a/tests/test_mfa.py b/tests/test_mfa.py index e12469b1..a5d92cde 100644 --- a/tests/test_mfa.py +++ b/tests/test_mfa.py @@ -5,14 +5,14 @@ import time from flask import url_for, session, request # These imports are required, because otherwise we get circular imports?! -from uffd import ldap, user +from uffd import user from uffd.user.models import User from uffd.role.models import Role, RoleGroup from uffd.mfa.models import MFAMethod, MFAType, RecoveryCodeMethod, TOTPMethod, WebauthnMethod, _hotp from uffd import create_app, db -from utils import dump, UffdTestCase +from utils import dump, UffdTestCase, db_flush class TestMfaPrimitives(unittest.TestCase): def test_hotp(self): @@ -34,7 +34,7 @@ def get_fido2_test_cred(self): class TestMfaMethodModels(UffdTestCase): def test_common_attributes(self): - method = MFAMethod(user=self.get_user(), name='testname') + method = TOTPMethod(user=self.get_user(), name='testname') self.assertTrue(method.created <= datetime.datetime.now()) self.assertEqual(method.name, 'testname') self.assertEqual(method.user.loginname, 'testuser') @@ -54,24 +54,28 @@ class TestMfaMethodModels(UffdTestCase): def test_totp_method_attributes(self): method = TOTPMethod(user=self.get_user(), name='testname') + raw_key = method.raw_key + issuer = method.issuer + accountname = method.accountname + key_uri = method.key_uri self.assertEqual(method.name, 'testname') # Restore method with key parameter _method = TOTPMethod(user=self.get_user(), key=method.key, name='testname') self.assertEqual(_method.name, 'testname') - self.assertEqual(method.raw_key, _method.raw_key) - self.assertEqual(method.issuer, _method.issuer) - self.assertEqual(method.accountname, _method.accountname) - self.assertEqual(method.key_uri, _method.key_uri) + self.assertEqual(_method.raw_key, raw_key) + self.assertEqual(_method.issuer, issuer) + self.assertEqual(_method.accountname, accountname) + self.assertEqual(_method.key_uri, key_uri) db.session.add(method) db.session.commit() db.session = db.create_scoped_session() # Ensure the next query does not return the cached method object # Restore method from db _method = TOTPMethod.query.get(method.id) self.assertEqual(_method.name, 'testname') - self.assertEqual(method.raw_key, _method.raw_key) - self.assertEqual(method.issuer, _method.issuer) - self.assertEqual(method.accountname, _method.accountname) - self.assertEqual(method.key_uri, _method.key_uri) + self.assertEqual(_method.raw_key, raw_key) + self.assertEqual(_method.issuer, issuer) + self.assertEqual(_method.accountname, accountname) + self.assertEqual(_method.key_uri, key_uri) def test_totp_method_verify(self): method = TOTPMethod(user=self.get_user()) @@ -163,15 +167,15 @@ class TestMfaViews(UffdTestCase): self.login_as('user') self.add_recovery_codes() self.add_totp() - admin_methods = len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()) + admin_methods = len(MFAMethod.query.filter_by(user=self.get_admin()).all()) r = self.client.get(path=url_for('mfa.disable'), follow_redirects=True) dump('mfa_disable', r) self.assertEqual(r.status_code, 200) r = self.client.post(path=url_for('mfa.disable_confirm'), follow_redirects=True) dump('mfa_disable_submit', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(MFAMethod.query.filter_by(dn=request.user.dn).all()), 0) - self.assertEqual(len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()), admin_methods) + self.assertEqual(len(MFAMethod.query.filter_by(user=request.user).all()), 0) + self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_admin()).all()), admin_methods) def test_disable_recovery_only(self): baserole = Role(name='baserole', is_default=True) @@ -180,19 +184,19 @@ class TestMfaViews(UffdTestCase): db.session.commit() self.login_as('user') self.add_recovery_codes() - admin_methods = len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()) - self.assertNotEqual(len(MFAMethod.query.filter_by(dn=request.user.dn).all()), 0) + admin_methods = len(MFAMethod.query.filter_by(user=self.get_admin()).all()) + self.assertNotEqual(len(MFAMethod.query.filter_by(user=request.user).all()), 0) r = self.client.get(path=url_for('mfa.disable'), follow_redirects=True) dump('mfa_disable_recovery_only', r) self.assertEqual(r.status_code, 200) r = self.client.post(path=url_for('mfa.disable_confirm'), follow_redirects=True) dump('mfa_disable_recovery_only_submit', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(MFAMethod.query.filter_by(dn=request.user.dn).all()), 0) - self.assertEqual(len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()), admin_methods) + self.assertEqual(len(MFAMethod.query.filter_by(user=request.user).all()), 0) + self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_admin()).all()), admin_methods) def test_admin_disable(self): - for method in MFAMethod.query.filter_by(dn=self.get_admin().dn).all(): + for method in MFAMethod.query.filter_by(user=self.get_admin()).all(): if not isinstance(method, RecoveryCodeMethod): db.session.delete(method) db.session.commit() @@ -200,20 +204,20 @@ class TestMfaViews(UffdTestCase): self.add_totp() self.login_as('admin') self.assertIsNotNone(request.user) - admin_methods = len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()) - r = self.client.get(path=url_for('mfa.admin_disable', uid=self.get_user().uid), follow_redirects=True) + admin_methods = len(MFAMethod.query.filter_by(user=self.get_admin()).all()) + r = self.client.get(path=url_for('mfa.admin_disable', id=self.get_user().id), follow_redirects=True) dump('mfa_admin_disable', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(MFAMethod.query.filter_by(dn=self.get_user().dn).all()), 0) - self.assertEqual(len(MFAMethod.query.filter_by(dn=self.get_admin().dn).all()), admin_methods) + self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_user()).all()), 0) + self.assertEqual(len(MFAMethod.query.filter_by(user=self.get_admin()).all()), admin_methods) def test_setup_recovery(self): self.login_as('user') - self.assertEqual(len(RecoveryCodeMethod.query.filter_by(dn=request.user.dn).all()), 0) + self.assertEqual(len(RecoveryCodeMethod.query.filter_by(user=request.user).all()), 0) r = self.client.post(path=url_for('mfa.setup_recovery'), follow_redirects=True) dump('mfa_setup_recovery', r) self.assertEqual(r.status_code, 200) - methods = RecoveryCodeMethod.query.filter_by(dn=request.user.dn).all() + methods = RecoveryCodeMethod.query.filter_by(user=request.user).all() self.assertNotEqual(len(methods), 0) r = self.client.post(path=url_for('mfa.setup_recovery'), follow_redirects=True) dump('mfa_setup_recovery_reset', r) @@ -242,30 +246,30 @@ class TestMfaViews(UffdTestCase): db.session.commit() self.login_as('user') self.add_recovery_codes() - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) method = TOTPMethod(request.user, key=session.get('mfa_totp_key', '')) code = _hotp(int(time.time()/30), method.raw_key) r = self.client.post(path=url_for('mfa.setup_totp_finish', name='My TOTP Authenticator'), data={'code': code}, follow_redirects=True) dump('mfa_setup_totp_finish', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 1) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 1) def test_setup_totp_finish_without_recovery(self): self.login_as('user') - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) method = TOTPMethod(request.user, key=session.get('mfa_totp_key', '')) code = _hotp(int(time.time()/30), method.raw_key) r = self.client.post(path=url_for('mfa.setup_totp_finish', name='My TOTP Authenticator'), data={'code': code}, follow_redirects=True) dump('mfa_setup_totp_finish_without_recovery', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) def test_setup_totp_finish_wrong_code(self): self.login_as('user') self.add_recovery_codes() - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) method = TOTPMethod(request.user, key=session.get('mfa_totp_key', '')) code = _hotp(int(time.time()/30), method.raw_key) @@ -273,17 +277,19 @@ class TestMfaViews(UffdTestCase): r = self.client.post(path=url_for('mfa.setup_totp_finish', name='My TOTP Authenticator'), data={'code': code}, follow_redirects=True) dump('mfa_setup_totp_finish_wrong_code', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + db_flush() + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) def test_setup_totp_finish_empty_code(self): self.login_as('user') self.add_recovery_codes() - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) r = self.client.get(path=url_for('mfa.setup_totp', name='My TOTP Authenticator'), follow_redirects=True) r = self.client.post(path=url_for('mfa.setup_totp_finish', name='My TOTP Authenticator'), data={'code': ''}, follow_redirects=True) dump('mfa_setup_totp_finish_empty_code', r) self.assertEqual(r.status_code, 200) - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 0) + db_flush() + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 0) def test_delete_totp(self): baserole = Role(name='baserole', is_default=True) @@ -296,12 +302,12 @@ class TestMfaViews(UffdTestCase): method = TOTPMethod(request.user, name='test') db.session.add(method) db.session.commit() - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 2) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 2) r = self.client.get(path=url_for('mfa.delete_totp', id=method.id), follow_redirects=True) dump('mfa_delete_totp', r) self.assertEqual(r.status_code, 200) self.assertEqual(len(TOTPMethod.query.filter_by(id=method.id).all()), 0) - self.assertEqual(len(TOTPMethod.query.filter_by(dn=request.user.dn).all()), 1) + self.assertEqual(len(TOTPMethod.query.filter_by(user=request.user).all()), 1) # TODO: webauthn setup tests @@ -424,6 +430,3 @@ class TestMfaViews(UffdTestCase): self.assertIsNone(request.user) # TODO: webauthn auth tests - -class TestMfaViewsOL(TestMfaViews): - use_openldap = True diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py index 592f8610..47a97f2a 100644 --- a/tests/test_oauth2.py +++ b/tests/test_oauth2.py @@ -4,12 +4,12 @@ from urllib.parse import urlparse, parse_qs from flask import url_for, session # These imports are required, because otherwise we get circular imports?! -from uffd import ldap, user +from uffd import user from uffd.user.models import User from uffd.session.models import DeviceLoginConfirmation from uffd.oauth2.models import OAuth2Client, OAuth2DeviceLoginInitiation -from uffd import create_app, db, ldap +from uffd import create_app, db from utils import dump, UffdTestCase @@ -69,7 +69,7 @@ class TestViews(UffdTestCase): self.assertEqual(r.status_code, 200) self.assertEqual(r.content_type, 'application/json') user = self.get_user() - self.assertEqual(r.json['id'], user.uid) + self.assertEqual(r.json['id'], user.unix_uid) self.assertEqual(r.json['name'], user.displayname) self.assertEqual(r.json['nickname'], user.loginname) self.assertEqual(r.json['email'], user.mail) diff --git a/tests/test_role.py b/tests/test_role.py index 0adbde99..7ede7f7a 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -5,7 +5,6 @@ import unittest from flask import url_for, session # These imports are required, because otherwise we get circular imports?! -from uffd.ldap import ldap from uffd import user from uffd.user.models import User, Group @@ -37,12 +36,10 @@ class TestPrimitives(unittest.TestCase): class TestUserRoleAttributes(UffdTestCase): def test_roles_effective(self): - for user in User.query.filter_by(loginname='service').all(): - ldap.session.delete(user) - ldap.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) - ldap.session.commit() + db.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) + db.session.commit() user = self.get_user() - service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + service_user = User.query.filter_by(loginname='service').one_or_none() included_by_default_role = Role(name='included_by_default') default_role = Role(name='default', is_default=True, included_roles=[included_by_default_role]) included_role = Role(name='included') @@ -53,8 +50,6 @@ class TestUserRoleAttributes(UffdTestCase): db.session.add_all([included_by_default_role, default_role, included_role, cycle_role, direct_role1, direct_role2]) self.assertSetEqual(user.roles_effective, {direct_role1, direct_role2, cycle_role, included_role, default_role, included_by_default_role}) self.assertSetEqual(service_user.roles_effective, {direct_role1, direct_role2, cycle_role, included_role}) - ldap.session.delete(service_user) - ldap.session.commit() def test_compute_groups(self): user = self.get_user() @@ -64,8 +59,8 @@ class TestUserRoleAttributes(UffdTestCase): role2 = Role(name='role2', groups={group1: RoleGroup(group=group1), group2: RoleGroup(group=group2)}) db.session.add_all([role1, role2]) self.assertSetEqual(user.compute_groups(), set()) - role1.members.add(user) - role2.members.add(user) + role1.members.append(user) + role2.members.append(user) self.assertSetEqual(user.compute_groups(), {group1, group2}) role2.groups[group2].requires_mfa = True self.assertSetEqual(user.compute_groups(), {group1}) @@ -79,7 +74,7 @@ class TestUserRoleAttributes(UffdTestCase): role1 = Role(name='role1', members=[user], groups={group1: RoleGroup(group=group1)}) role2 = Role(name='role2', groups={group2: RoleGroup(group=group2)}) db.session.add_all([role1, role2]) - user.groups = {group2} + user.groups = [group2] groups_added, groups_removed = user.update_groups() self.assertSetEqual(groups_added, {group1}) self.assertSetEqual(groups_removed, {group2}) @@ -91,13 +86,11 @@ class TestUserRoleAttributes(UffdTestCase): class TestRoleModel(UffdTestCase): def test_members_effective(self): - for user in User.query.filter_by(loginname='service').all(): - ldap.session.delete(user) - ldap.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) - ldap.session.commit() + db.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) + db.session.commit() user1 = self.get_user() user2 = self.get_admin() - service = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + service = User.query.filter_by(loginname='service').one_or_none() included_by_default_role = Role(name='included_by_default') default_role = Role(name='default', is_default=True, included_roles=[included_by_default_role]) included_role = Role(name='included') @@ -108,8 +101,6 @@ class TestRoleModel(UffdTestCase): self.assertSetEqual(included_role.members_effective, {user1, user2, service}) self.assertSetEqual(direct_role.members_effective, {user1, user2, service}) self.assertSetEqual(empty_role.members_effective, set()) - ldap.session.delete(service) - ldap.session.commit() def test_included_roles_recursive(self): baserole = Role(name='base') @@ -189,23 +180,22 @@ class TestRoleViews(UffdTestCase): db.session.commit() self.assertEqual(role.name, 'base') self.assertEqual(role.description, 'Base role description') - self.assertEqual([group.dn for group in role.groups], [self.test_data.get('group_uffd_admin').get('dn')]) + self.assertSetEqual(set(role.groups), {self.get_admin_group()}) r = self.client.post(path=url_for('role.update', roleid=role.id), - data={'name': 'base1', 'description': 'Base role description1', 'moderator-group': '', 'group-20001': '1', 'group-20002': '1'}, + data={'name': 'base1', 'description': 'Base role description1', 'moderator-group': '', 'group-%d'%self.get_users_group().id: '1', 'group-%d'%self.get_access_group().id: '1'}, follow_redirects=True) dump('role_update', r) self.assertEqual(r.status_code, 200) role = Role.query.get(role.id) self.assertEqual(role.name, 'base1') self.assertEqual(role.description, 'Base role description1') - self.assertEqual(sorted([group.dn for group in role.groups]), [self.test_data.get('group_uffd_access').get('dn'), - self.test_data.get('group_users').get('dn')]) + self.assertSetEqual(set(role.groups), {self.get_access_group(), self.get_users_group()}) # TODO: verify that group memberships are updated (currently not possible with ldap mock!) def test_create(self): self.assertIsNone(Role.query.filter_by(name='base').first()) r = self.client.post(path=url_for('role.update'), - data={'name': 'base', 'description': 'Base role description', 'moderator-group': '', 'group-20001': '1', 'group-20002': '1'}, + data={'name': 'base', 'description': 'Base role description', 'moderator-group': '', 'group-%d'%self.get_users_group().id: '1', 'group-%d'%self.get_access_group().id: '1'}, follow_redirects=True) dump('role_create', r) self.assertEqual(r.status_code, 200) @@ -213,14 +203,13 @@ class TestRoleViews(UffdTestCase): self.assertIsNotNone(role) self.assertEqual(role.name, 'base') self.assertEqual(role.description, 'Base role description') - self.assertEqual(sorted([group.dn for group in role.groups]), [self.test_data.get('group_uffd_access').get('dn'), - self.test_data.get('group_users').get('dn')]) + self.assertSetEqual(set(role.groups), {self.get_access_group(), self.get_users_group()}) # TODO: verify that group memberships are updated (currently not possible with ldap mock!) def test_create_with_moderator_group(self): self.assertIsNone(Role.query.filter_by(name='base').first()) r = self.client.post(path=url_for('role.update'), - data={'name': 'base', 'description': 'Base role description', 'moderator-group': self.test_data.get('group_uffd_admin').get('dn'), 'group-20001': '1', 'group-20002': '1'}, + data={'name': 'base', 'description': 'Base role description', 'moderator-group': self.get_admin_group().id, 'group-%d'%self.get_users_group().id: '1', 'group-%d'%self.get_access_group().id: '1'}, follow_redirects=True) self.assertEqual(r.status_code, 200) role = Role.query.filter_by(name='base').first() @@ -228,8 +217,7 @@ class TestRoleViews(UffdTestCase): self.assertEqual(role.name, 'base') self.assertEqual(role.description, 'Base role description') self.assertEqual(role.moderator_group.name, 'uffd_admin') - self.assertEqual(sorted([group.dn for group in role.groups]), [self.test_data.get('group_uffd_access').get('dn'), - self.test_data.get('group_users').get('dn')]) + self.assertSetEqual(set(role.groups), {self.get_access_group(), self.get_users_group()}) # TODO: verify that group memberships are updated (currently not possible with ldap mock!) def test_delete(self): @@ -245,54 +233,46 @@ class TestRoleViews(UffdTestCase): # TODO: verify that group memberships are updated (currently not possible with ldap mock!) def test_set_default(self): - for user in User.query.filter_by(loginname='service').all(): - ldap.session.delete(user) - ldap.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) - ldap.session.commit() + db.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) + db.session.commit() role = Role(name='test') db.session.add(role) role.groups[self.get_admin_group()] = RoleGroup() user1 = self.get_user() user2 = self.get_admin() - service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) - self.assertSetEqual(set(user1.roles_effective), set()) - self.assertSetEqual(set(user2.roles_effective), set()) + service_user = User.query.filter_by(loginname='service').one_or_none() + self.assertSetEqual(set(self.get_user().roles_effective), set()) + self.assertSetEqual(set(self.get_admin().roles_effective), set()) self.assertSetEqual(set(service_user.roles_effective), set()) - role.members.add(user1) - role.members.add(service_user) - self.assertSetEqual(set(user1.roles_effective), {role}) - self.assertSetEqual(set(user2.roles_effective), set()) + role.members.append(self.get_user()) + role.members.append(service_user) + self.assertSetEqual(set(self.get_user().roles_effective), {role}) + self.assertSetEqual(set(self.get_admin().roles_effective), set()) self.assertSetEqual(set(service_user.roles_effective), {role}) db.session.commit() role_id = role.id - self.assertSetEqual(set(role.members), {user1, service_user}) + self.assertSetEqual(set(role.members), {self.get_user(), service_user}) r = self.client.get(path=url_for('role.set_default', roleid=role.id), follow_redirects=True) dump('role_set_default', r) self.assertEqual(r.status_code, 200) role = Role.query.get(role_id) - service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + service_user = User.query.filter_by(loginname='service').one_or_none() self.assertSetEqual(set(role.members), {service_user}) - self.assertSetEqual(set(user1.roles_effective), {role}) - self.assertSetEqual(set(user2.roles_effective), {role}) - ldap.session.delete(service_user) - ldap.session.commit() + self.assertSetEqual(set(self.get_user().roles_effective), {role}) + self.assertSetEqual(set(self.get_admin().roles_effective), {role}) def test_unset_default(self): - for user in User.query.filter_by(loginname='service').all(): - ldap.session.delete(user) - ldap.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) - ldap.session.commit() admin_role = Role(name='admin', is_default=True) db.session.add(admin_role) admin_role.groups[self.get_admin_group()] = RoleGroup() + db.session.add(User(loginname='service', is_service_user=True, mail='service@example.com', displayname='Service')) + db.session.commit() role = Role(name='test', is_default=True) db.session.add(role) - user1 = self.get_user() - user2 = self.get_admin() - service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) - role.members.add(service_user) - self.assertSetEqual(set(user1.roles_effective), {role, admin_role}) - self.assertSetEqual(set(user2.roles_effective), {role, admin_role}) + service_user = User.query.filter_by(loginname='service').one_or_none() + role.members.append(service_user) + self.assertSetEqual(set(self.get_user().roles_effective), {role, admin_role}) + self.assertSetEqual(set(self.get_admin().roles_effective), {role, admin_role}) self.assertSetEqual(set(service_user.roles_effective), {role}) db.session.commit() role_id = role.id @@ -303,12 +283,7 @@ class TestRoleViews(UffdTestCase): self.assertEqual(r.status_code, 200) role = Role.query.get(role_id) admin_role = Role.query.get(admin_role_id) - service_user = User.query.get('uid=service,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + service_user = User.query.filter_by(loginname='service').one_or_none() self.assertSetEqual(set(role.members), {service_user}) - self.assertSetEqual(set(user1.roles_effective), {admin_role}) - self.assertSetEqual(set(user2.roles_effective), {admin_role}) - ldap.session.delete(service_user) - ldap.session.commit() - -class TestRoleViewsOL(TestRoleViews): - use_openldap = True + self.assertSetEqual(set(self.get_user().roles_effective), {admin_role}) + self.assertSetEqual(set(self.get_admin().roles_effective), {admin_role}) diff --git a/tests/test_rolemod.py b/tests/test_rolemod.py index 81a39c97..beb47219 100644 --- a/tests/test_rolemod.py +++ b/tests/test_rolemod.py @@ -3,7 +3,6 @@ from flask import url_for from uffd.user.models import User, Group from uffd.role.models import Role, RoleGroup from uffd.database import db -from uffd.ldap import ldap from utils import dump, UffdTestCase @@ -41,7 +40,7 @@ class TestRolemodViews(UffdTestCase): def test_show(self): role = Role(name='test', moderator_group=self.get_access_group()) db.session.add(role) - role.members.add(self.get_admin()) + role.members.append(self.get_admin()) db.session.commit() r = self.client.get(path=url_for('rolemod.show', role_id=role.id), follow_redirects=True) dump('rolemod_show', r) @@ -119,16 +118,16 @@ class TestRolemodViews(UffdTestCase): role = Role(name='test', moderator_group=self.get_access_group()) role.groups[self.get_admin_group()] = RoleGroup() db.session.add(role) - role.members.add(self.get_admin()) + role.members.append(self.get_admin()) db.session.commit() role.update_member_groups() - ldap.session.commit() + db.session.commit() user = self.get_admin() group = self.get_admin_group() self.assertTrue(user in group.members) role = Role.query.get(role.id) self.assertTrue(user in role.members) - r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) + r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True) dump('rolemod_delete_member', r) self.assertEqual(r.status_code, 200) user_updated = self.get_admin() @@ -143,7 +142,7 @@ class TestRolemodViews(UffdTestCase): db.session.add(role) db.session.commit() user = self.get_admin() - r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) + r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True) dump('rolemod_delete_member_nomember', r) self.assertEqual(r.status_code, 200) @@ -152,12 +151,12 @@ class TestRolemodViews(UffdTestCase): db.session.add(Role(name='other_role', moderator_group=self.get_access_group())) role = Role(name='test', moderator_group=self.get_admin_group()) db.session.add(role) - role.members.add(self.get_admin()) + role.members.append(self.get_admin()) db.session.commit() user = self.get_admin() role = Role.query.get(role.id) self.assertTrue(user in role.members) - r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) + r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True) dump('rolemod_delete_member_noperm', r) self.assertEqual(r.status_code, 403) user_updated = self.get_admin() @@ -169,12 +168,12 @@ class TestRolemodViews(UffdTestCase): db.session.add(Role(name='other_role', moderator_group=self.get_access_group())) role = Role(name='test') db.session.add(role) - role.members.add(self.get_admin()) + role.members.append(self.get_admin()) db.session.commit() user = self.get_admin() role = Role.query.get(role.id) self.assertTrue(user in role.members) - r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_dn=user.dn), follow_redirects=True) + r = self.client.get(path=url_for('rolemod.delete_member', role_id=role.id, member_id=user.id), follow_redirects=True) dump('rolemod_delete_member_nomod', r) self.assertEqual(r.status_code, 403) user_updated = self.get_admin() diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py index 2d381d05..2365d7cc 100644 --- a/tests/test_selfservice.py +++ b/tests/test_selfservice.py @@ -4,7 +4,7 @@ import unittest from flask import url_for, request # These imports are required, because otherwise we get circular imports?! -from uffd import ldap, user +from uffd import user from uffd.selfservice.models import MailToken, PasswordToken from uffd.user.models import User @@ -57,7 +57,7 @@ class TestSelfservice(UffdTestCase): self.assertEqual(r.status_code, 200) _user = request.user self.assertNotEqual(_user.mail, 'newemail@example.com') - token = MailToken.query.filter(MailToken.loginname == user.loginname).first() + token = MailToken.query.filter(MailToken.user == user).first() self.assertEqual(token.newmail, 'newemail@example.com') self.assertIn(token.token, str(self.app.last_mail.get_content())) r = self.client.get(path=url_for('selfservice.token_mail', token_id=token.id, token=token.token), follow_redirects=True) @@ -87,7 +87,7 @@ class TestSelfservice(UffdTestCase): dump('change_password', r) self.assertEqual(r.status_code, 200) _user = request.user - self.assertTrue(ldap.test_user_bind(_user.dn, 'newpassword')) + self.assertTrue(_user.check_password('newpassword')) def test_change_password_invalid(self): self.login_as('user') @@ -98,8 +98,8 @@ class TestSelfservice(UffdTestCase): dump('change_password_invalid', r) self.assertEqual(r.status_code, 200) _user = request.user - self.assertFalse(ldap.test_user_bind(_user.dn, 'shortpw')) - self.assertTrue(ldap.test_user_bind(_user.dn, 'userpassword')) + self.assertFalse(_user.check_password('shortpw')) + self.assertTrue(_user.check_password('userpassword')) # Regression test for #100 (login not possible if password contains character disallowed by SASLprep) def test_change_password_samlprep_invalid(self): @@ -111,8 +111,8 @@ class TestSelfservice(UffdTestCase): dump('change_password_samlprep_invalid', r) self.assertEqual(r.status_code, 200) _user = request.user - self.assertFalse(ldap.test_user_bind(_user.dn, 'shortpw\n')) - self.assertTrue(ldap.test_user_bind(_user.dn, 'userpassword')) + self.assertFalse(_user.check_password('shortpw\n')) + self.assertTrue(_user.check_password('userpassword')) def test_change_password_mismatch(self): self.login_as('user') @@ -123,13 +123,11 @@ class TestSelfservice(UffdTestCase): dump('change_password_mismatch', r) self.assertEqual(r.status_code, 200) _user = request.user - self.assertFalse(ldap.test_user_bind(_user.dn, 'newpassword1')) - self.assertFalse(ldap.test_user_bind(_user.dn, 'newpassword2')) - self.assertTrue(ldap.test_user_bind(_user.dn, 'userpassword')) + self.assertFalse(_user.check_password('newpassword1')) + self.assertFalse(_user.check_password('newpassword2')) + self.assertTrue(_user.check_password('userpassword')) def test_leave_role(self): - if self.use_userconnection: - self.skipTest('Leaving roles is not possible in user mode') baserole = Role(name='baserole', is_default=True) db.session.add(baserole) baserole.groups[self.get_access_group()] = RoleGroup() @@ -160,35 +158,39 @@ class TestSelfservice(UffdTestCase): def test_token_mail_invalid(self): self.login_as('user') user = request.user - token = MailToken(loginname=user.loginname, newmail='newusermail@example.com') + old_mail = user.mail + token = MailToken(user=user, newmail='newusermail@example.com') db.session.add(token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_mail', token_id=token.id, token='A'*128), follow_redirects=True) dump('token_mail_invalid', r) self.assertEqual(r.status_code, 200) _user = request.user - self.assertEqual(_user.mail, user.mail) + self.assertEqual(_user.mail, old_mail) def test_token_mail_wrong_user(self): self.login_as('user') user = request.user + old_mail = user.mail admin_user = self.get_admin() - db.session.add(MailToken(loginname=user.loginname, newmail='newusermail@example.com')) - admin_token = MailToken(loginname='testadmin', newmail='newadminmail@example.com') + old_admin_mail = admin_user.mail + db.session.add(MailToken(user=user, newmail='newusermail@example.com')) + admin_token = MailToken(user=admin_user, newmail='newadminmail@example.com') db.session.add(admin_token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_mail', token_id=admin_token.id, token=admin_token.token), follow_redirects=True) dump('token_mail_wrong_user', r) self.assertEqual(r.status_code, 403) - _user = request.user + _user = self.get_user() _admin_user = self.get_admin() - self.assertEqual(_user.mail, user.mail) - self.assertEqual(_admin_user.mail, admin_user.mail) + self.assertEqual(_user.mail, old_mail) + self.assertEqual(_admin_user.mail, old_admin_mail) def test_token_mail_expired(self): self.login_as('user') user = request.user - token = MailToken(loginname=user.loginname, newmail='newusermail@example.com', + old_mail = user.mail + token = MailToken(user=user, newmail='newusermail@example.com', created=(datetime.datetime.now() - datetime.timedelta(days=10))) db.session.add(token) db.session.commit() @@ -196,13 +198,11 @@ class TestSelfservice(UffdTestCase): dump('token_mail_expired', r) self.assertEqual(r.status_code, 200) _user = request.user - self.assertEqual(_user.mail, user.mail) - tokens = MailToken.query.filter(MailToken.loginname == user.loginname).all() + self.assertEqual(_user.mail, old_mail) + tokens = MailToken.query.filter(MailToken.user == _user).all() self.assertEqual(len(tokens), 0) def test_forgot_password(self): - if self.use_userconnection: - self.skipTest('Password Reset is not possible in user mode') user = self.get_user() r = self.client.get(path=url_for('selfservice.forgot_password')) dump('forgot_password', r) @@ -211,13 +211,11 @@ class TestSelfservice(UffdTestCase): data={'loginname': user.loginname, 'mail': user.mail}, follow_redirects=True) dump('forgot_password_submit', r) self.assertEqual(r.status_code, 200) - token = PasswordToken.query.filter(PasswordToken.loginname == user.loginname).first() + token = PasswordToken.query.filter(PasswordToken.user == user).first() self.assertIsNotNone(token) self.assertIn(token.token, str(self.app.last_mail.get_content())) def test_forgot_password_wrong_user(self): - if self.use_userconnection: - self.skipTest('Password Reset is not possible in user mode') user = self.get_user() r = self.client.get(path=url_for('selfservice.forgot_password')) self.assertEqual(r.status_code, 200) @@ -229,8 +227,6 @@ class TestSelfservice(UffdTestCase): self.assertEqual(len(PasswordToken.query.all()), 0) def test_forgot_password_wrong_email(self): - if self.use_userconnection: - self.skipTest('Password Reset is not possible in user mode') user = self.get_user() r = self.client.get(path=url_for('selfservice.forgot_password'), follow_redirects=True) self.assertEqual(r.status_code, 200) @@ -243,8 +239,6 @@ class TestSelfservice(UffdTestCase): # Regression test for #31 def test_forgot_password_invalid_user(self): - if self.use_userconnection: - self.skipTest('Password Reset is not possible in user mode') r = self.client.post(path=url_for('selfservice.forgot_password'), data={'loginname': '=', 'mail': 'test@example.com'}, follow_redirects=True) dump('forgot_password_submit_invalid_user', r) @@ -253,10 +247,8 @@ class TestSelfservice(UffdTestCase): self.assertEqual(len(PasswordToken.query.all()), 0) def test_token_password(self): - if self.use_userconnection: - self.skipTest('Password Token is not possible in user mode') user = self.get_user() - token = PasswordToken(loginname=user.loginname) + token = PasswordToken(user=user) db.session.add(token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token=token.token), follow_redirects=True) @@ -266,11 +258,9 @@ class TestSelfservice(UffdTestCase): data={'password1': 'newpassword', 'password2': 'newpassword'}, follow_redirects=True) dump('token_password_submit', r) self.assertEqual(r.status_code, 200) - self.assertTrue(ldap.test_user_bind(user.dn, 'newpassword')) + self.assertTrue(self.get_user().check_password('newpassword')) def test_token_password_emptydb(self): - if self.use_userconnection: - self.skipTest('Password Token is not possible in user mode') user = self.get_user() r = self.client.get(path=url_for('selfservice.token_password', token_id=1, token='A'*128), follow_redirects=True) dump('token_password_emptydb', r) @@ -281,13 +271,11 @@ class TestSelfservice(UffdTestCase): dump('token_password_emptydb_submit', r) self.assertEqual(r.status_code, 200) self.assertIn(b'Token expired, please try again', r.data) - self.assertTrue(ldap.test_user_bind(user.dn, 'userpassword')) + self.assertTrue(self.get_user().check_password('userpassword')) def test_token_password_invalid(self): - if self.use_userconnection: - self.skipTest('Password Token is not possible in user mode') user = self.get_user() - token = PasswordToken(loginname=user.loginname) + token = PasswordToken(user=user) db.session.add(token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token='A'*128), follow_redirects=True) @@ -299,14 +287,11 @@ class TestSelfservice(UffdTestCase): dump('token_password_invalid_submit', r) self.assertEqual(r.status_code, 200) self.assertIn(b'Token expired, please try again', r.data) - self.assertTrue(ldap.test_user_bind(user.dn, 'userpassword')) + self.assertTrue(self.get_user().check_password('userpassword')) def test_token_password_expired(self): - if self.use_userconnection: - self.skipTest('Password Token is not possible in user mode') user = self.get_user() - token = PasswordToken(loginname=user.loginname, - created=(datetime.datetime.now() - datetime.timedelta(days=10))) + token = PasswordToken(user=user, created=(datetime.datetime.now() - datetime.timedelta(days=10))) db.session.add(token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token=token.token), follow_redirects=True) @@ -318,13 +303,11 @@ class TestSelfservice(UffdTestCase): dump('token_password_invalid_expired_submit', r) self.assertEqual(r.status_code, 200) self.assertIn(b'Token expired, please try again', r.data) - self.assertTrue(ldap.test_user_bind(user.dn, 'userpassword')) + self.assertTrue(self.get_user().check_password('userpassword')) def test_token_password_different_passwords(self): - if self.use_userconnection: - self.skipTest('Password Token is not possible in user mode') user = self.get_user() - token = PasswordToken(loginname=user.loginname) + token = PasswordToken(user=user) db.session.add(token) db.session.commit() r = self.client.get(path=url_for('selfservice.token_password', token_id=token.id, token=token.token), follow_redirects=True) @@ -333,12 +316,4 @@ class TestSelfservice(UffdTestCase): data={'password1': 'newpassword', 'password2': 'differentpassword'}, follow_redirects=True) dump('token_password_different_passwords_submit', r) self.assertEqual(r.status_code, 200) - self.assertTrue(ldap.test_user_bind(user.dn, 'userpassword')) - - -class TestSelfserviceOL(TestSelfservice): - use_openldap = True - - -class TestSelfserviceOLUser(TestSelfserviceOL): - use_userconnection = True + self.assertTrue(self.get_user().check_password('userpassword')) diff --git a/tests/test_services.py b/tests/test_services.py index ab565077..89b83fb2 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -4,7 +4,7 @@ import unittest from flask import url_for # These imports are required, because otherwise we get circular imports?! -from uffd import ldap, user +from uffd import user from utils import dump, UffdTestCase diff --git a/tests/test_session.py b/tests/test_session.py index c7707ea3..cf7a9697 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -4,7 +4,7 @@ import unittest from flask import url_for, request # These imports are required, because otherwise we get circular imports?! -from uffd import ldap, user +from uffd import user from uffd.session.views import login_required from uffd.session.models import DeviceLoginConfirmation @@ -63,7 +63,7 @@ class TestSession(UffdTestCase): def test_titlecase_password(self): r = self.client.post(path=url_for('session.login'), - data={'loginname': self.test_data.get('user').get('loginname').title(), 'password': self.test_data.get('user').get('password')}, follow_redirects=True) + data={'loginname': self.get_user().loginname.title(), 'password': 'userpassword'}, follow_redirects=True) self.assertEqual(r.status_code, 200) self.assertLoggedIn() @@ -74,7 +74,7 @@ class TestSession(UffdTestCase): def test_wrong_password(self): r = self.client.post(path=url_for('session.login'), - data={'loginname': self.test_data.get('user').get('loginname'), 'password': 'wrongpassword'}, + data={'loginname': self.get_user().loginname, 'password': 'wrongpassword'}, follow_redirects=True) dump('login_wrong_password', r) self.assertEqual(r.status_code, 200) @@ -82,7 +82,7 @@ class TestSession(UffdTestCase): def test_empty_password(self): r = self.client.post(path=url_for('session.login'), - data={'loginname': self.test_data.get('user').get('loginname'), 'password': ''}, follow_redirects=True) + data={'loginname': self.get_user().loginname, 'password': ''}, follow_redirects=True) dump('login_empty_password', r) self.assertEqual(r.status_code, 200) self.assertLoggedOut() @@ -90,14 +90,14 @@ class TestSession(UffdTestCase): # Regression test for #100 (uncatched LDAPSASLPrepError) def test_saslprep_invalid_password(self): r = self.client.post(path=url_for('session.login'), - data={'loginname': self.test_data.get('user').get('loginname'), 'password': 'wrongpassword\n'}, follow_redirects=True) + data={'loginname': 'testuser', 'password': 'wrongpassword\n'}, follow_redirects=True) dump('login_saslprep_invalid_password', r) self.assertEqual(r.status_code, 200) self.assertLoggedOut() def test_wrong_user(self): r = self.client.post(path=url_for('session.login'), - data={'loginname': 'nouser', 'password': self.test_data.get('user').get('password')}, + data={'loginname': 'nouser', 'password': 'userpassword'}, follow_redirects=True) dump('login_wrong_user', r) self.assertEqual(r.status_code, 200) @@ -105,7 +105,7 @@ class TestSession(UffdTestCase): def test_empty_user(self): r = self.client.post(path=url_for('session.login'), - data={'loginname': '', 'password': self.test_data.get('user').get('password')}, follow_redirects=True) + data={'loginname': '', 'password': 'userpassword'}, follow_redirects=True) dump('login_empty_user', r) self.assertEqual(r.status_code, 200) self.assertLoggedOut() @@ -140,7 +140,7 @@ class TestSession(UffdTestCase): def test_ratelimit(self): for i in range(20): self.client.post(path=url_for('session.login'), - data={'loginname': self.test_data.get('user').get('loginname'), + data={'loginname': self.get_user().loginname, 'password': 'wrongpassword_%i'%i}, follow_redirects=True) r = self.login_as('user') dump('login_ratelimit', r) @@ -173,9 +173,3 @@ class TestSession(UffdTestCase): r = self.client.get(path=url_for('session.deviceauth_finish'), follow_redirects=True) self.assertEqual(r.status_code, 200) self.assertEqual(DeviceLoginConfirmation.query.all(), []) - -class TestSessionOL(TestSession): - use_openldap = True - -class TestSessionOLUser(TestSessionOL): - use_userconnection = True diff --git a/tests/test_signup.py b/tests/test_signup.py index 79cbac75..063635a2 100644 --- a/tests/test_signup.py +++ b/tests/test_signup.py @@ -6,7 +6,6 @@ from flask import url_for, session, request # These imports are required, because otherwise we get circular imports?! from uffd import user -from uffd.ldap import ldap from uffd import create_app, db from uffd.signup.models import Signup @@ -41,18 +40,18 @@ class TestSignupModel(UffdTestCase): def assert_finish_success(self, signup, password): self.assertIsNone(signup.user) user, msg = signup.finish(password) - ldap.session.commit() + db.session.commit() self.assertIsNotNone(user) self.assertIsInstance(msg, str) self.assertIsNotNone(signup.user) def assert_finish_failure(self, signup, password): - prev_dn = signup.user_dn + prev_id = signup.user_id user, msg = signup.finish(password) self.assertIsNone(user) self.assertIsInstance(msg, str) self.assertNotEqual(msg, '') - self.assertEqual(signup.user_dn, prev_dn) + self.assertEqual(signup.user_id, prev_id) def test_password(self): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com') @@ -74,7 +73,7 @@ class TestSignupModel(UffdTestCase): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') self.assertFalse(signup.completed) signup.finish('notsecret') - ldap.session.commit() + db.session.commit() self.assertTrue(signup.completed) signup = refetch_signup(signup) self.assertTrue(signup.completed) @@ -123,15 +122,11 @@ class TestSignupModel(UffdTestCase): def test_finish(self): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') - if self.use_openldap: - self.assertIsNone(login_get_user('newuser', 'notsecret')) self.assert_finish_success(signup, 'notsecret') - user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser').one_or_none() self.assertEqual(user.loginname, 'newuser') self.assertEqual(user.displayname, 'New User') self.assertEqual(user.mail, 'test@example.com') - if self.use_openldap: - self.assertIsNotNone(login_get_user('newuser', 'notsecret')) def test_finish_completed(self): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') @@ -179,12 +174,9 @@ class TestSignupModel(UffdTestCase): db_flush() signup = Signup.query.get(signup1_id) self.assert_finish_failure(signup, 'notsecret') - user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser').one_or_none() self.assertEqual(user.mail, 'test2@example.com') -class TestSignupModelOL(TestSignupModel): - use_openldap = True - class TestSignupViews(UffdTestCase): def setUpApp(self): self.app.config['SELF_SIGNUP'] = True @@ -331,15 +323,13 @@ class TestSignupViews(UffdTestCase): signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = refetch_signup(signup) self.assertFalse(signup.completed) - if self.use_openldap: - self.assertIsNone(login_get_user('newuser', 'notsecret')) + self.assertIsNone(login_get_user('newuser', 'notsecret')) r = self.client.get(path=url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token), follow_redirects=True) dump('test_signup_confirm', r) self.assertEqual(r.status_code, 200) signup = refetch_signup(signup) self.assertFalse(signup.completed) - if self.use_openldap: - self.assertIsNone(login_get_user('newuser', 'notsecret')) + self.assertIsNone(login_get_user('newuser', 'notsecret')) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) dump('test_signup_confirm_submit', r) self.assertEqual(r.status_code, 200) @@ -348,19 +338,16 @@ class TestSignupViews(UffdTestCase): self.assertEqual(signup.user.loginname, 'newuser') self.assertEqual(signup.user.displayname, 'New User') self.assertEqual(signup.user.mail, 'test@example.com') - if self.use_openldap: - self.assertIsNotNone(login_get_user('newuser', 'notsecret')) - self.assertIsNotNone(request.user) - self.assertEqual(request.user.loginname, 'newuser') + self.assertIsNotNone(login_get_user('newuser', 'notsecret')) def test_confirm_loggedin(self): baserole = Role(name='baserole', is_default=True) db.session.add(baserole) baserole.groups[self.get_access_group()] = RoleGroup() db.session.commit() + self.login_as('user') signup = Signup(loginname='newuser', displayname='New User', mail='test@example.com', password='notsecret') signup = refetch_signup(signup) - self.login_as('user') self.assertFalse(signup.completed) self.assertIsNotNone(request.user) self.assertEqual(request.user.loginname, self.get_user().loginname) @@ -409,6 +396,7 @@ class TestSignupViews(UffdTestCase): signup = refetch_signup(signup) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'wrongpassword'}) dump('test_signup_confirm_wrongpassword', r) + signup = refetch_signup(signup) self.assertEqual(r.status_code, 200) self.assertFalse(signup.completed) @@ -418,6 +406,7 @@ class TestSignupViews(UffdTestCase): signup = refetch_signup(signup) r = self.client.post(path=url_for('signup.signup_confirm_submit', signup_id=signup.id, token=signup.token), follow_redirects=True, data={'password': 'notsecret'}) dump('test_signup_confirm_error', r) + signup = refetch_signup(signup) self.assertEqual(r.status_code, 200) self.assertFalse(signup.completed) @@ -447,6 +436,3 @@ class TestSignupViews(UffdTestCase): dump('test_signup_confirm_confirmlimit', r) self.assertEqual(r.status_code, 200) self.assertFalse(signup.completed) - -class TestSignupViewsOL(TestSignupViews): - use_openldap = True diff --git a/tests/test_user.py b/tests/test_user.py index 5ed757dd..139b5c46 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -4,15 +4,14 @@ import unittest from flask import url_for, session # These imports are required, because otherwise we get circular imports?! -from uffd import ldap, user +from uffd import user -from uffd.user.models import User +from uffd.user.models import User, Group from uffd.role.models import Role from uffd import create_app, db from utils import dump, UffdTestCase - class TestUserModel(UffdTestCase): def test_has_permission(self): user_ = self.get_user() # has 'users' and 'uffd_access' group @@ -38,16 +37,6 @@ class TestUserModel(UffdTestCase): self.assertFalse(user_.has_permission(['uffd_admin', ['users', 'notagroup']])) self.assertTrue(admin.has_permission(['uffd_admin', ['users', 'notagroup']])) -class TestUserModelOL(TestUserModel): - use_openldap = True - -class TestUserModelOLUser(TestUserModelOL): - use_userconnection = True - - def setUp(self): - super().setUp() - self.login_as('admin') - class TestUserViews(UffdTestCase): def setUp(self): super().setUp() @@ -69,24 +58,24 @@ class TestUserViews(UffdTestCase): r = self.client.get(path=url_for('user.show'), follow_redirects=True) dump('user_new', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) + self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none()) r = self.client.post(path=url_for('user.update'), data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'New User', f'role-{role1_id}': '1', 'password': 'newpassword'}, follow_redirects=True) dump('user_new_submit', r) self.assertEqual(r.status_code, 200) - user_ = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user_ = User.query.filter_by(loginname='newuser').one_or_none() roles = sorted([r.name for r in user_.roles_effective]) self.assertIsNotNone(user_) self.assertFalse(user_.is_service_user) self.assertEqual(user_.loginname, 'newuser') self.assertEqual(user_.displayname, 'New User') self.assertEqual(user_.mail, 'newuser@example.com') - self.assertTrue(user_.uid) + self.assertGreaterEqual(user_.unix_uid, self.app.config['USER_MIN_UID']) + self.assertLessEqual(user_.unix_uid, self.app.config['USER_MAX_UID']) role1 = Role(name='role1') self.assertEqual(roles, ['base', 'role1']) # TODO: confirm Mail is send, login not yet possible - #self.assertTrue(ldap.test_user_bind(user_.dn, 'newpassword')) def test_new_service(self): db.session.add(Role(name='base', is_default=True)) @@ -99,24 +88,23 @@ class TestUserViews(UffdTestCase): r = self.client.get(path=url_for('user.show'), follow_redirects=True) dump('user_new_service', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) + self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none()) r = self.client.post(path=url_for('user.update'), data={'loginname': 'newuser', 'mail': 'newuser@example.com', 'displayname': 'New User', f'role-{role1_id}': '1', 'password': 'newpassword', 'serviceaccount': '1'}, follow_redirects=True) dump('user_new_submit', r) self.assertEqual(r.status_code, 200) - user = User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser').one_or_none() roles = sorted([r.name for r in user.roles]) self.assertIsNotNone(user) self.assertTrue(user.is_service_user) self.assertEqual(user.loginname, 'newuser') self.assertEqual(user.displayname, 'New User') self.assertEqual(user.mail, 'newuser@example.com') - self.assertTrue(user.uid) + self.assertTrue(user.unix_uid) role1 = Role(name='role1') self.assertEqual(roles, ['role1']) # TODO: confirm Mail is send, login not yet possible - #self.assertTrue(ldap.test_user_bind(user.dn, 'newpassword')) def test_new_invalid_loginname(self): r = self.client.post(path=url_for('user.update'), @@ -124,7 +112,7 @@ class TestUserViews(UffdTestCase): 'password': 'newpassword'}, follow_redirects=True) dump('user_new_invalid_loginname', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) + self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none()) def test_new_empty_loginname(self): r = self.client.post(path=url_for('user.update'), @@ -132,7 +120,7 @@ class TestUserViews(UffdTestCase): 'password': 'newpassword'}, follow_redirects=True) dump('user_new_empty_loginname', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) + self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none()) def test_new_empty_email(self): r = self.client.post(path=url_for('user.update'), @@ -140,7 +128,7 @@ class TestUserViews(UffdTestCase): 'password': 'newpassword'}, follow_redirects=True) dump('user_new_empty_email', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) + self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none()) def test_new_invalid_display_name(self): r = self.client.post(path=url_for('user.update'), @@ -148,7 +136,7 @@ class TestUserViews(UffdTestCase): 'password': 'newpassword'}, follow_redirects=True) dump('user_new_invalid_display_name', r) self.assertEqual(r.status_code, 200) - self.assertIsNone(User.query.get('uid=newuser,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) + self.assertIsNone(User.query.filter_by(loginname='newuser').one_or_none()) def test_update(self): user_unupdated = self.get_user() @@ -157,13 +145,13 @@ class TestUserViews(UffdTestCase): db.session.add(role1) role2 = Role(name='role2') db.session.add(role2) - role2.members.add(user_unupdated) + role2.members.append(user_unupdated) db.session.commit() role1_id = role1.id - r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True) dump('user_update', r) self.assertEqual(r.status_code, 200) - r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), + r = self.client.post(path=url_for('user.update', id=user_unupdated.id), data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', f'role-{role1_id}': '1', 'password': ''}, follow_redirects=True) dump('user_update_submit', r) @@ -172,17 +160,16 @@ class TestUserViews(UffdTestCase): roles = sorted([r.name for r in user_updated.roles_effective]) self.assertEqual(user_updated.displayname, 'New User') self.assertEqual(user_updated.mail, 'newuser@example.com') - self.assertEqual(user_updated.uid, user_unupdated.uid) + self.assertEqual(user_updated.unix_uid, user_unupdated.unix_uid) self.assertEqual(user_updated.loginname, user_unupdated.loginname) - print(user_updated.dn) - self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password'))) + self.assertTrue(user_updated.check_password('userpassword')) self.assertEqual(roles, ['base', 'role1']) def test_update_password(self): user_unupdated = self.get_user() - r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True) self.assertEqual(r.status_code, 200) - r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), + r = self.client.post(path=url_for('user.update', id=user_unupdated.id), data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', 'password': 'newpassword'}, follow_redirects=True) dump('user_update_password', r) @@ -190,23 +177,23 @@ class TestUserViews(UffdTestCase): user_updated = self.get_user() self.assertEqual(user_updated.displayname, 'New User') self.assertEqual(user_updated.mail, 'newuser@example.com') - self.assertEqual(user_updated.uid, user_unupdated.uid) + self.assertEqual(user_updated.unix_uid, user_unupdated.unix_uid) self.assertEqual(user_updated.loginname, user_unupdated.loginname) - self.assertTrue(ldap.test_user_bind(user_updated.dn, 'newpassword')) - self.assertFalse(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password'))) + self.assertTrue(user_updated.check_password('newpassword')) + self.assertFalse(user_updated.check_password('userpassword')) def test_update_invalid_password(self): user_unupdated = self.get_user() - r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True) self.assertEqual(r.status_code, 200) - r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), + r = self.client.post(path=url_for('user.update', id=user_unupdated.id), data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', 'password': 'A'}, follow_redirects=True) dump('user_update_invalid_password', r) self.assertEqual(r.status_code, 200) user_updated = self.get_user() - self.assertFalse(ldap.test_user_bind(user_updated.dn, 'A')) - self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password'))) + self.assertFalse(user_updated.check_password('A')) + self.assertTrue(user_updated.check_password('userpassword')) self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.loginname, user_unupdated.loginname) @@ -214,25 +201,25 @@ class TestUserViews(UffdTestCase): # Regression test for #100 (login not possible if password contains character disallowed by SASLprep) def test_update_saslprep_invalid_password(self): user_unupdated = self.get_user() - r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True) self.assertEqual(r.status_code, 200) - r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), + r = self.client.post(path=url_for('user.update', id=user_unupdated.id), data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'New User', 'password': 'newpassword\n'}, follow_redirects=True) - dump('user_update_saslprep_invalid_password', r) + dump('user_update_invalid_password', r) self.assertEqual(r.status_code, 200) user_updated = self.get_user() - self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword\n')) - self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password'))) + self.assertFalse(user_updated.check_password('newpassword\n')) + self.assertTrue(user_updated.check_password('userpassword')) self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.loginname, user_unupdated.loginname) def test_update_empty_email(self): user_unupdated = self.get_user() - r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True) self.assertEqual(r.status_code, 200) - r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), + r = self.client.post(path=url_for('user.update', id=user_unupdated.id), data={'loginname': 'testuser', 'mail': '', 'displayname': 'New User', 'password': 'newpassword'}, follow_redirects=True) dump('user_update_empty_mail', r) @@ -241,14 +228,14 @@ class TestUserViews(UffdTestCase): self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.loginname, user_unupdated.loginname) - self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword')) - self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password'))) + self.assertFalse(user_updated.check_password('newpassword')) + self.assertTrue(user_updated.check_password('userpassword')) def test_update_invalid_display_name(self): user_unupdated = self.get_user() - r = self.client.get(path=url_for('user.show', uid=user_unupdated.uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=user_unupdated.id), follow_redirects=True) self.assertEqual(r.status_code, 200) - r = self.client.post(path=url_for('user.update', uid=user_unupdated.uid), + r = self.client.post(path=url_for('user.update', id=user_unupdated.id), data={'loginname': 'testuser', 'mail': 'newuser@example.com', 'displayname': 'A'*200, 'password': 'newpassword'}, follow_redirects=True) dump('user_update_invalid_display_name', r) @@ -257,16 +244,16 @@ class TestUserViews(UffdTestCase): self.assertEqual(user_updated.displayname, user_unupdated.displayname) self.assertEqual(user_updated.mail, user_unupdated.mail) self.assertEqual(user_updated.loginname, user_unupdated.loginname) - self.assertFalse(ldap.test_user_bind(user_updated.dn, 'newpassword')) - self.assertTrue(ldap.test_user_bind(user_updated.dn, self.test_data.get('user').get('password'))) + self.assertFalse(user_updated.check_password('newpassword')) + self.assertTrue(user_updated.check_password('userpassword')) def test_show(self): - r = self.client.get(path=url_for('user.show', uid=self.get_user().uid), follow_redirects=True) + r = self.client.get(path=url_for('user.show', id=self.get_user().id), follow_redirects=True) dump('user_show', r) self.assertEqual(r.status_code, 200) def test_delete(self): - r = self.client.get(path=url_for('user.delete', uid=self.get_user().uid), follow_redirects=True) + r = self.client.get(path=url_for('user.delete', id=self.get_user().id), follow_redirects=True) dump('user_delete', r) self.assertEqual(r.status_code, 200) self.assertIsNone(self.get_user()) @@ -297,59 +284,59 @@ newuser12,newuser12@example.com,{role1.id};{role1.id} r = self.client.post(path=url_for('user.csvimport'), data={'csv': data}, follow_redirects=True) dump('user_csvimport', r) self.assertEqual(r.status_code, 200) - user = User.query.get('uid=newuser1,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser1').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser1') self.assertEqual(user.displayname, 'newuser1') self.assertEqual(user.mail, 'newuser1@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, []) - user = User.query.get('uid=newuser2,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser2').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser2') self.assertEqual(user.displayname, 'newuser2') self.assertEqual(user.mail, 'newuser2@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, ['role1']) - user = User.query.get('uid=newuser3,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser3').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser3') self.assertEqual(user.displayname, 'newuser3') self.assertEqual(user.mail, 'newuser3@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, ['role1', 'role2']) - user = User.query.get('uid=newuser4,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser4').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser4') self.assertEqual(user.displayname, 'newuser4') self.assertEqual(user.mail, 'newuser4@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, []) - user = User.query.get('uid=newuser5,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser5').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser5') self.assertEqual(user.displayname, 'newuser5') self.assertEqual(user.mail, 'newuser5@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, []) - user = User.query.get('uid=newuser6,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser6').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser6') self.assertEqual(user.displayname, 'newuser6') self.assertEqual(user.mail, 'newuser6@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, ['role1', 'role2']) - self.assertIsNone(User.query.get('uid=newuser7,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) - self.assertIsNone(User.query.get('uid=newuser8,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) - self.assertIsNone(User.query.get('uid=newuser9,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE']))) - user = User.query.get('uid=newuser10,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + self.assertIsNone(User.query.filter_by(loginname='newuser7').one_or_none()) + self.assertIsNone(User.query.filter_by(loginname='newuser8').one_or_none()) + self.assertIsNone(User.query.filter_by(loginname='newuser9').one_or_none()) + user = User.query.filter_by(loginname='newuser10').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser10') self.assertEqual(user.displayname, 'newuser10') self.assertEqual(user.mail, 'newuser10@example.com') roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, []) - user = User.query.get('uid=newuser11,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser11').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser11') self.assertEqual(user.displayname, 'newuser11') @@ -357,7 +344,7 @@ newuser12,newuser12@example.com,{role1.id};{role1.id} # Currently the csv import is not very robust, imho newuser11 should have role1 and role2! roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, ['role2']) - user = User.query.get('uid=newuser12,{}'.format(self.app.config['LDAP_USER_SEARCH_BASE'])) + user = User.query.filter_by(loginname='newuser12').one_or_none() self.assertIsNotNone(user) self.assertEqual(user.loginname, 'newuser12') self.assertEqual(user.displayname, 'newuser12') @@ -365,12 +352,6 @@ newuser12,newuser12@example.com,{role1.id};{role1.id} roles = sorted([r.name for r in user.roles]) self.assertEqual(roles, ['role1']) -class TestUserViewsOL(TestUserViews): - use_openldap = True - -class TestUserViewsOLUser(TestUserViewsOL): - use_userconnection = True - class TestGroupViews(UffdTestCase): def setUp(self): super().setUp() @@ -386,8 +367,94 @@ class TestGroupViews(UffdTestCase): dump('group_show', r) self.assertEqual(r.status_code, 200) -class TestGroupViewsOL(TestGroupViews): - use_openldap = True + def test_new(self): + r = self.client.get(path=url_for('group.show'), follow_redirects=True) + dump('group_new', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(Group.query.filter_by(name='newgroup').one_or_none()) + r = self.client.post(path=url_for('group.update'), + data={'unix_gid': '', 'name': 'newgroup', 'description': 'Test description'}, + follow_redirects=True) + dump('group_new_submit', r) + self.assertEqual(r.status_code, 200) + group = Group.query.filter_by(name='newgroup').one_or_none() + self.assertIsNotNone(group) + self.assertEqual(group.name, 'newgroup') + self.assertEqual(group.description, 'Test description') + self.assertGreaterEqual(group.unix_gid, self.app.config['GROUP_MIN_GID']) + self.assertLessEqual(group.unix_gid, self.app.config['GROUP_MAX_GID']) + + def test_new_fixed_gid(self): + gid = self.app.config['GROUP_MAX_GID'] - 1 + r = self.client.post(path=url_for('group.update'), + data={'unix_gid': str(gid), 'name': 'newgroup', 'description': 'Test description'}, + follow_redirects=True) + dump('group_new_fixed_gid', r) + self.assertEqual(r.status_code, 200) + group = Group.query.filter_by(name='newgroup').one_or_none() + self.assertIsNotNone(group) + self.assertEqual(group.name, 'newgroup') + self.assertEqual(group.description, 'Test description') + self.assertEqual(group.unix_gid, gid) + + def test_new_existing_name(self): + gid = self.app.config['GROUP_MAX_GID'] - 1 + db.session.add(Group(name='newgroup', description='Original description', unix_gid=gid)) + db.session.commit() + r = self.client.post(path=url_for('group.update'), + data={'unix_gid': '', 'name': 'newgroup', 'description': 'New description'}, + follow_redirects=True) + dump('group_new_existing_name', r) + self.assertEqual(r.status_code, 400) + group = Group.query.filter_by(name='newgroup').one_or_none() + self.assertIsNotNone(group) + self.assertEqual(group.name, 'newgroup') + self.assertEqual(group.description, 'Original description') + self.assertEqual(group.unix_gid, gid) + + def test_new_existing_gid(self): + gid = self.app.config['GROUP_MAX_GID'] - 1 + db.session.add(Group(name='newgroup', description='Original description', unix_gid=gid)) + db.session.commit() + r = self.client.post(path=url_for('group.update'), + data={'unix_gid': str(gid), 'name': 'newgroup2', 'description': 'New description'}, + follow_redirects=True) + dump('group_new_existing_gid', r) + self.assertEqual(r.status_code, 400) + group = Group.query.filter_by(name='newgroup').one_or_none() + self.assertIsNotNone(group) + self.assertEqual(group.name, 'newgroup') + self.assertEqual(group.description, 'Original description') + self.assertEqual(group.unix_gid, gid) + self.assertIsNone(Group.query.filter_by(name='newgroup2').one_or_none()) -class TestGroupViewsOLUser(TestGroupViewsOL): - use_userconnection = True + def test_update(self): + group = Group(name='newgroup', description='Original description') + db.session.add(group) + db.session.commit() + group_id = group.id + group_gid = group.unix_gid + new_gid = self.app.config['GROUP_MAX_GID'] - 1 + r = self.client.post(path=url_for('group.update', id=group_id), + data={'unix_gid': str(new_gid), 'name': 'newgroup_changed', 'description': 'New description'}, + follow_redirects=True) + dump('group_update', r) + self.assertEqual(r.status_code, 200) + group = Group.query.get(group_id) + self.assertEqual(group.name, 'newgroup') # Not changed + self.assertEqual(group.description, 'New description') # Changed + self.assertEqual(group.unix_gid, group_gid) # Not changed + + def test_delete(self): + group1 = Group(name='newgroup1', description='Original description1') + group2 = Group(name='newgroup2', description='Original description2') + db.session.add(group1) + db.session.add(group2) + db.session.commit() + group1_id = group1.id + group2_id = group2.id + r = self.client.get(path=url_for('group.delete', id=group1_id), follow_redirects=True) + dump('group_delete', r) + self.assertEqual(r.status_code, 200) + self.assertIsNone(Group.query.get(group1_id)) + self.assertIsNotNone(Group.query.get(group2_id)) diff --git a/tests/utils.py b/tests/utils.py index 61c89e44..82194221 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,6 +7,7 @@ from flask import request, url_for from uffd import create_app, db from uffd.user.models import User, Group +from uffd.mail.models import Mail def dump(basename, resp): basename = basename.replace('.', '_').replace('/', '_') @@ -22,32 +23,37 @@ def dump(basename, resp): def db_flush(): db.session.rollback() db.session = db.create_scoped_session() - if hasattr(request, 'ldap_connection'): - del request.ldap_session class UffdTestCase(unittest.TestCase): - use_openldap = False - use_userconnection = False - def get_user(self): - return User.query.get(self.test_data.get('user').get('dn')) + return User.query.filter_by(loginname='testuser').one_or_none() def get_admin(self): - return User.query.get(self.test_data.get('admin').get('dn')) + return User.query.filter_by(loginname='testadmin').one_or_none() def get_admin_group(self): - return Group.query.get(self.test_data.get('group_uffd_admin').get('dn')) + return Group.query.filter_by(name='uffd_admin').one_or_none() def get_access_group(self): - return Group.query.get(self.test_data.get('group_uffd_access').get('dn')) + return Group.query.filter_by(name='uffd_access').one_or_none() def get_users_group(self): - return Group.query.get(self.test_data.get('group_users').get('dn')) + return Group.query.filter_by(name='users').one_or_none() + + def get_mail(self): + return Mail.query.filter_by(uid='test').one_or_none() def login_as(self, user, ref=None): + loginname = None + password = None + if user == 'user': + loginname = 'testuser' + password = 'userpassword' + elif user == 'admin': + loginname = 'testadmin' + password = 'adminpassword' return self.client.post(path=url_for('session.login', ref=ref), - data={'loginname': self.test_data.get(user).get('loginname'), - 'password': self.test_data.get(user).get('password')}, follow_redirects=True) + data={'loginname': loginname, 'password': password}, follow_redirects=True) def setUp(self): # It would be far better to create a minimal app here, but since the @@ -57,52 +63,11 @@ class UffdTestCase(unittest.TestCase): 'DEBUG': True, 'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:', 'SECRET_KEY': 'DEBUGKEY', - 'LDAP_SERVICE_MOCK': True, 'MAIL_SKIP_SEND': True, 'SELF_SIGNUP': True, 'ENABLE_INVITE': True, 'ENABLE_PASSWORDRESET': True } - if self.use_openldap: - if not os.environ.get('UNITTEST_OPENLDAP'): - self.skipTest('OPENLDAP_TESTING not set') - config['LDAP_SERVICE_MOCK'] = False - config['LDAP_SERVICE_URL'] = 'ldap://localhost' - if self.use_userconnection: - config['LDAP_SERVICE_USER_BIND'] = True - config['SELF_SIGNUP'] = False - config['ENABLE_INVITE'] = False - config['ENABLE_PASSWORDRESET'] = False - config['ENABLE_ROLESELFSERVICE'] = False - else: - config['LDAP_SERVICE_BIND_DN'] = 'cn=uffd,ou=system,dc=example,dc=com' - config['LDAP_SERVICE_BIND_PASSWORD'] = 'uffd-ldap-password' - os.system("ldapdelete -c -D 'cn=uffd,ou=system,dc=example,dc=com' -w '{}' -H '{}' -f tests/openldap_ldifs/ldap_server_entries_cleanup.ldif > /dev/null 2>&1".format(config['LDAP_SERVICE_BIND_PASSWORD'], config['LDAP_SERVICE_URL'])) - os.system("ldapadd -c -D 'cn=uffd,ou=system,dc=example,dc=com' -w '{}' -H '{}' -f tests/openldap_ldifs/ldap_server_entries_add.ldif".format(config['LDAP_SERVICE_BIND_PASSWORD'], config['LDAP_SERVICE_URL'])) - os.system("ldapmodify -c -D 'cn=uffd,ou=system,dc=example,dc=com' -w '{}' -H '{}' -f tests/openldap_ldifs/ldap_server_entries_modify.ldif".format(config['LDAP_SERVICE_BIND_PASSWORD'], config['LDAP_SERVICE_URL'])) - #os.system("/usr/sbin/slapcat -n 1 -l /dev/stdout") - - self.test_data = { - 'admin': { - 'loginname': 'testadmin', - 'dn': 'uid=testadmin,ou=users,dc=example,dc=com', - 'password': 'adminpassword' - }, - 'user': { - 'loginname': 'testuser', - 'dn': 'uid=testuser,ou=users,dc=example,dc=com', - 'password': 'userpassword' - }, - 'group_uffd_access': { - 'dn': 'cn=uffd_access,ou=groups,dc=example,dc=com' - }, - 'group_uffd_admin': { - 'dn': 'cn=uffd_admin,ou=groups,dc=example,dc=com' - }, - 'group_users': { - 'dn': 'cn=users,ou=groups,dc=example,dc=com' - } - } self.app = create_app(config) self.setUpApp() @@ -111,6 +76,20 @@ class UffdTestCase(unittest.TestCase): # Just do some request so that we can use url_for self.client.get(path='/') db.create_all() + # This reflects the old LDAP example data + users_group = Group(name='users', unix_gid=20001, description='Base group for all users') + db.session.add(users_group) + access_group = Group(name='uffd_access', unix_gid=20002, description='Access to Single-Sign-On and Selfservice') + db.session.add(access_group) + admin_group = Group(name='uffd_admin', unix_gid=20003, description='Admin access to uffd') + db.session.add(admin_group) + testuser = User(loginname='testuser', unix_uid=10000, password='userpassword', mail='test@example.com', displayname='Test User', groups=[users_group, access_group]) + db.session.add(testuser) + testadmin = User(loginname='testadmin', unix_uid=10001, password='adminpassword', mail='admin@example.com', displayname='Test Admin', groups=[users_group, access_group, admin_group]) + db.session.add(testadmin) + testmail = Mail(uid='test', receivers=['test1@example.com', 'test2@example.com'], destinations=['testuser@mail.example.com']) + db.session.add(testmail) + db.session.commit() def setUpApp(self): pass diff --git a/uffd/__init__.py b/uffd/__init__.py index 9b31eecf..1769045a 100644 --- a/uffd/__init__.py +++ b/uffd/__init__.py @@ -13,14 +13,13 @@ except ImportError: from werkzeug.exceptions import InternalServerError, Forbidden from flask_migrate import Migrate -import uffd.ldap from uffd.database import db, SQLAlchemyJSON from uffd.template_helper import register_template_helper from uffd.navbar import setup_navbar from uffd.secure_redirect import secure_local_redirect from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, services, signup, rolemod, invite, api from uffd.user.models import User, Group -from uffd.role.models import Role +from uffd.role.models import Role, RoleGroup from uffd.mail.models import Mail def load_config_file(app, cfg_name, silent=False): @@ -41,7 +40,7 @@ def load_config_file(app, cfg_name, silent=False): app.config.from_pyfile(cfg_path, silent=True) return True -def create_app(test_config=None): # pylint: disable=too-many-locals +def create_app(test_config=None): # pylint: disable=too-many-locals,too-many-statements # create and configure the app app = Flask(__name__, instance_relative_config=False) app.json_encoder = SQLAlchemyJSON @@ -81,12 +80,6 @@ def create_app(test_config=None): # pylint: disable=too-many-locals for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + mfa.bp + oauth2.bp + services.bp + rolemod.bp + api.bp: app.register_blueprint(i) - if app.config['LDAP_SERVICE_USER_BIND'] and (app.config['ENABLE_INVITE'] or - app.config['SELF_SIGNUP'] or - app.config['ENABLE_PASSWORDRESET'] or - app.config['ENABLE_ROLESELFSERVICE']): - raise InternalServerError(description="You cannot use INVITES, SIGNUP, PASSWORDRESET or ROLESELFSERVICE when using a USER_BIND!") - if app.config['ENABLE_INVITE'] or app.config['SELF_SIGNUP']: for i in signup.bp: app.register_blueprint(i) @@ -97,7 +90,7 @@ def create_app(test_config=None): # pylint: disable=too-many-locals @app.shell_context_processor def push_request_context(): #pylint: disable=unused-variable app.test_request_context().push() # LDAP ORM requires request context - return {'db': db, 'ldap': uffd.ldap.ldap, 'User': User, 'Group': Group, 'Role': Role, 'Mail': Mail} + return {'db': db, 'User': User, 'Group': Group, 'Role': Role, 'Mail': Mail} @app.errorhandler(403) def handle_403(error): @@ -114,11 +107,6 @@ def create_app(test_config=None): # pylint: disable=too-many-locals resp.set_cookie('language', request.values['lang']) return resp - @app.teardown_request - def close_connection(exception): #pylint: disable=unused-variable,unused-argument - if hasattr(request, "ldap_connection"): - request.ldap_connection.unbind() - @app.cli.command("gendevcert", help='Generates a self-signed TLS certificate for development') def gendevcert(): #pylint: disable=unused-variable if os.path.exists('devcert.crt') or os.path.exists('devcert.key'): @@ -137,6 +125,28 @@ def create_app(test_config=None): # pylint: disable=too-many-locals app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) app.run(debug=True) + @app.cli.command("create-examples", help='Create example users, groups and roles') + def create_examples(): #pylint: disable=unused-variable + assert app.debug + with app.test_request_context(): + access_group = Group(name='uffd_access', description='Access to Single-Sign-On and Selfservice') + db.session.add(access_group) + admin_group = Group(name='uffd_admin', description='Admin access to uffd') + db.session.add(admin_group) + base_role = Role(name='base', is_default=True, groups={access_group: RoleGroup(group=access_group)}, description='Base role for all regular users') + db.session.add(base_role) + admin_role = Role(name='admin', groups={admin_group: RoleGroup(group=admin_group)}, description='Admin role') + db.session.add(admin_role) + testuser = User(loginname='testuser', password='userpassword', mail='test@example.com', displayname='Test User') + testuser.update_groups() + db.session.add(testuser) + testadmin = User(loginname='testadmin', password='adminpassword', mail='admin@example.com', displayname='Test Admin', roles=[admin_role]) + testadmin.update_groups() + db.session.add(testadmin) + testmail = Mail(uid='test', receivers=['test1@example.com', 'test2@example.com'], destinations=['testuser@mail.example.com']) + db.session.add(testmail) + db.session.commit() + babel = Babel(app) @babel.localeselector diff --git a/uffd/api/views.py b/uffd/api/views.py index bc9aa0dd..d63003d2 100644 --- a/uffd/api/views.py +++ b/uffd/api/views.py @@ -4,7 +4,7 @@ import secrets from flask import Blueprint, jsonify, current_app, request, abort from uffd.user.models import User, Group -from uffd.mail.models import Mail +from uffd.mail.models import Mail, MailReceiveAddress, MailDestinationAddress from uffd.session.views import login_get_user, login_ratelimit bp = Blueprint('api', __name__, template_folder='templates', url_prefix='/api/v1/') @@ -29,7 +29,7 @@ def apikey_required(scope=None): return wrapper def generate_group_dict(group): - return {'id': group.gid, 'name': group.name, + return {'id': group.unix_gid, 'name': group.name, 'members': [user.loginname for user in group.members]} @bp.route('/getgroups', methods=['GET', 'POST']) @@ -42,7 +42,7 @@ def getgroups(): if key is None: groups = Group.query.all() elif key == 'id' and len(values) == 1: - groups = Group.query.filter_by(gid=values[0]).all() + groups = Group.query.filter_by(unix_gid=values[0]).all() elif key == 'name' and len(values) == 1: groups = Group.query.filter_by(name=values[0]).all() elif key == 'member' and len(values) == 1: @@ -55,7 +55,7 @@ def getgroups(): def generate_user_dict(user, all_groups=None): if all_groups is None: all_groups = user.groups - return {'id': user.uid, 'loginname': user.loginname, 'email': user.mail, 'displayname': user.displayname, + return {'id': user.unix_uid, 'loginname': user.loginname, 'email': user.mail, 'displayname': user.displayname, 'groups': [group.name for group in all_groups if user in group.members]} @bp.route('/getusers', methods=['GET', 'POST']) @@ -68,7 +68,7 @@ def getusers(): if key is None: users = User.query.all() elif key == 'id' and len(values) == 1: - users = User.query.filter_by(uid=values[0]).all() + users = User.query.filter_by(unix_uid=values[0]).all() elif key == 'loginname' and len(values) == 1: users = User.query.filter_by(loginname=values[0]).all() elif key == 'email' and len(values) == 1: @@ -115,9 +115,9 @@ def getmails(): elif key == 'name' and len(values) == 1: mails = Mail.query.filter_by(uid=values[0]).all() elif key == 'receive_address' and len(values) == 1: - mails = Mail.query.filter_by(receivers=values[0]).all() + mails = Mail.query.filter(Mail.receivers.any(MailReceiveAddress.address==values[0])).all() elif key == 'destination_address' and len(values) == 1: - mails = Mail.query.filter_by(destinations=values[0]).all() + mails = Mail.query.filter(Mail.destinations.any(MailDestinationAddress.address==values[0])).all() else: abort(400) return jsonify([generate_mail_dict(mail) for mail in mails]) diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg index 1db33f35..e4a3971d 100644 --- a/uffd/default_config.cfg +++ b/uffd/default_config.cfg @@ -1,51 +1,10 @@ -LDAP_USER_SEARCH_BASE="ou=users,dc=example,dc=com" -LDAP_USER_SEARCH_FILTER=[("objectClass", "person")] -LDAP_USER_OBJECTCLASSES=["top", "inetOrgPerson", "organizationalPerson", "person", "posixAccount"] -LDAP_USER_MIN_UID=10000 -LDAP_USER_MAX_UID=18999 -LDAP_USER_SERVICE_MIN_UID=19000 -LDAP_USER_SERVICE_MAX_UID=19999 -LDAP_USER_GID=20001 -LDAP_USER_DN_ATTRIBUTE="uid" -LDAP_USER_UID_ATTRIBUTE="uidNumber" -LDAP_USER_UID_ALIASES=[] -LDAP_USER_LOGINNAME_ATTRIBUTE="uid" -LDAP_USER_LOGINNAME_ALIASES=[] -LDAP_USER_DISPLAYNAME_ATTRIBUTE="cn" -LDAP_USER_DISPLAYNAME_ALIASES=["givenName", "displayName"] -LDAP_USER_MAIL_ATTRIBUTE="mail" -LDAP_USER_MAIL_ALIASES=[] -LDAP_USER_DEFAULT_ATTRIBUTES={ - "sn": " ", - # All string values are subject to python str.format-style format expansion. To insert literal braces use "{{" and "}}". - # Variables: uid, loginname, displayname, mail and possibly other attributes of the User class - "homeDirectory": "/home/{loginname}", - "gidNumber": LDAP_USER_GID, - # "multiValueAttribute": ["value1", "value2"], -} - -LDAP_GROUP_SEARCH_BASE="ou=groups,dc=example,dc=com" -LDAP_GROUP_SEARCH_FILTER=[("objectClass","groupOfUniqueNames")] -LDAP_GROUP_GID_ATTRIBUTE="gidNumber" -LDAP_GROUP_NAME_ATTRIBUTE="cn" -LDAP_GROUP_DESCRIPTION_ATTRIBUTE="description" -LDAP_GROUP_MEMBER_ATTRIBUTE="uniqueMember" - -LDAP_MAIL_SEARCH_BASE="ou=postfix,dc=example,dc=com" -LDAP_MAIL_SEARCH_FILTER=[("objectClass","postfixVirtual")] -LDAP_MAIL_OBJECTCLASSES=["top", "postfixVirtual"] -LDAP_MAIL_DN_ATTRIBUTE="uid" -LDAP_MAIL_UID_ATTRIBUTE="uid" -LDAP_MAIL_RECEIVERS_ATTRIBUTE="mailacceptinggeneralid" -LDAP_MAIL_DESTINATIONS_ATTRIBUTE="maildrop" - -LDAP_SERVICE_URL="ldapi:///" -LDAP_SERVICE_USE_STARTTLS=True -LDAP_SERVICE_BIND_DN="" -LDAP_SERVICE_BIND_PASSWORD="" -# Connections use LDAP_SERVICE_BIND_DN if LDAP_SERVICE_USER_BIND=False, otherwise they use the users credentials. -# When using a user connection, some features are not available, since they require a service connection -LDAP_SERVICE_USER_BIND=False +USER_GID=20001 +USER_MIN_UID=10000 +USER_MAX_UID=18999 +USER_SERVICE_MIN_UID=19000 +USER_SERVICE_MAX_UID=19999 +GROUP_MIN_GID=20000 +GROUP_MAX_GID=49999 SESSION_LIFETIME_SECONDS=3600 # CSRF protection @@ -95,7 +54,7 @@ FOOTER_LINKS=[{"url": "https://example.com", "title": "example"}] OAUTH2_CLIENTS={ #'test_client_id' : {'client_secret': 'random_secret', 'redirect_uris': ['https://example.com/oauth']}, - # You can optionally restrict access to users with a certain group. Set 'required_group' to the name of an LDAP group name or a list of groups. + # You can optionally restrict access to users with a certain group. Set 'required_group' to the name a group or a list of group names. # ... 'required_group': 'test_access_group' ... only allows users with group "test_access_group" access # ... 'required_group': ['groupa', ['groupb', 'groupc']] ... allows users with group "groupa" as well as users with both "groupb" and "groupc" access # Set 'login_message' (or suffixed with a language code like 'login_message_de') to display a custom message on the login form. @@ -173,7 +132,6 @@ WELCOME_TEXT='See https://docs.example.com/ for further information.' #TEMPLATES_AUTO_RELOAD=True #SQLALCHEMY_ECHO=True #FLASK_ENV=development -#LDAP_SERVICE_MOCK=True # DO set in production diff --git a/uffd/invite/models.py b/uffd/invite/models.py index 99b5dd59..d2e4bd4f 100644 --- a/uffd/invite/models.py +++ b/uffd/invite/models.py @@ -4,15 +4,13 @@ from flask import current_app from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Boolean from sqlalchemy.orm import relationship -from uffd.ldapalchemy.dbutils import DBRelationship from uffd.database import db -from uffd.user.models import User from uffd.signup.models import Signup from uffd.utils import token_urlfriendly invite_roles = db.Table('invite_roles', - Column('invite_id', Integer(), ForeignKey('invite.id'), primary_key=True), - Column('role_id', Integer, ForeignKey('role.id'), primary_key=True) + Column('invite_id', Integer(), ForeignKey('invite.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), + Column('role_id', Integer, ForeignKey('role.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) ) class Invite(db.Model): @@ -20,16 +18,16 @@ class Invite(db.Model): id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), unique=True, nullable=False, default=token_urlfriendly) created = Column(DateTime, default=datetime.datetime.now, nullable=False) - creator_dn = Column(String(128), nullable=True) - creator = DBRelationship('creator_dn', User) + creator_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE'), nullable=True) + creator = relationship('User') valid_until = Column(DateTime, nullable=False) single_use = Column(Boolean, default=True, nullable=False) allow_signup = Column(Boolean, default=True, nullable=False) used = Column(Boolean, default=False, nullable=False) disabled = Column(Boolean, default=False, nullable=False) roles = relationship('Role', secondary=invite_roles) - signups = relationship('InviteSignup', back_populates='invite', lazy=True) - grants = relationship('InviteGrant', back_populates='invite', lazy=True) + signups = relationship('InviteSignup', back_populates='invite', lazy=True, cascade='all, delete-orphan') + grants = relationship('InviteGrant', back_populates='invite', lazy=True, cascade='all, delete-orphan') @property def expired(self): @@ -41,8 +39,6 @@ class Invite(db.Model): @property def permitted(self): - if self.creator_dn is None: - return True # Legacy invite link without creator if self.creator is None: return False # Creator does not exist (anymore) if self.creator.is_in_group(current_app.config['ACL_ADMIN_GROUP']): @@ -74,10 +70,10 @@ class Invite(db.Model): class InviteGrant(db.Model): __tablename__ = 'invite_grant' id = Column(Integer(), primary_key=True, autoincrement=True) - invite_id = Column(Integer(), ForeignKey('invite.id'), nullable=False) + invite_id = Column(Integer(), ForeignKey('invite.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) invite = relationship('Invite', back_populates='grants') - user_dn = Column(String(128), nullable=False) - user = DBRelationship('user_dn', User) + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + user = relationship('User') def apply(self): if not self.invite.active: @@ -87,15 +83,15 @@ class InviteGrant(db.Model): if set(self.invite.roles).issubset(self.user.roles): return False, 'Invite link does not grant any new roles' for role in self.invite.roles: - self.user.roles.add(role) + self.user.roles.append(role) self.user.update_groups() self.invite.used = True return True, 'Success' class InviteSignup(Signup): __tablename__ = 'invite_signup' - id = Column(Integer(), ForeignKey('signup.id'), primary_key=True) - invite_id = Column(Integer(), ForeignKey('invite.id'), nullable=False) + id = Column(Integer(), ForeignKey('signup.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) + invite_id = Column(Integer(), ForeignKey('invite.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) invite = relationship('Invite', back_populates='signups') __mapper_args__ = { @@ -113,7 +109,7 @@ class InviteSignup(Signup): user, msg = super().finish(password) if user is not None: for role in self.invite.roles: - user.roles.add(role) + user.roles.append(role) user.update_groups() self.invite.used = True return user, msg diff --git a/uffd/invite/templates/invite/list.html b/uffd/invite/templates/invite/list.html index a527fd45..9ddff030 100644 --- a/uffd/invite/templates/invite/list.html +++ b/uffd/invite/templates/invite/list.html @@ -30,9 +30,7 @@ {% endif %} </td> <td> - {% if not invite.creator_dn %} - {{ '<admin>' }} - {% elif not invite.creator %} + {% if not invite.creator %} {{ '<deleted user>' }} {% else %} {{ invite.creator.loginname }} @@ -105,10 +103,10 @@ {% else %} <ul> {% for signup in invite.signups if signup.completed %} - <li>{{_('Registration of user <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', uid=signup.user.uid)|e, user_name=signup.user.loginname|e)|safe}}</li> + <li>{{_('Registration of user <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', id=signup.user.id)|e, user_name=signup.user.loginname|e)|safe}}</li> {% endfor %} {% for grant in invite.grants if grant.user %} - <li>{{_('Roles granted to <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', uid=grant.user.uid)|e, user_name=grant.user.loginname|e)|safe}}</li> + <li>{{_('Roles granted to <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', id=grant.user.id)|e, user_name=grant.user.loginname|e)|safe}}</li> {% endfor %} </ul> {% endif %} diff --git a/uffd/invite/views.py b/uffd/invite/views.py index 185d1a22..8f38c7ef 100644 --- a/uffd/invite/views.py +++ b/uffd/invite/views.py @@ -7,11 +7,10 @@ import sqlalchemy from uffd.csrf import csrf_protect from uffd.database import db -from uffd.ldap import ldap from uffd.session import login_required from uffd.role.models import Role from uffd.invite.models import Invite, InviteSignup, InviteGrant -from uffd.user.models import User +from uffd.user.models import User, Group from uffd.sendmail import sendmail from uffd.navbar import register_navbar from uffd.ratelimit import host_ratelimit, format_delay @@ -27,21 +26,21 @@ def invite_acl_check(): return True if request.user.is_in_group(current_app.config['ACL_SIGNUP_GROUP']): return True - if Role.query.filter(Role.moderator_group_dn.in_(request.user.group_dns)).count(): + if Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).count(): return True return False def view_acl_filter(user): if user.is_in_group(current_app.config['ACL_ADMIN_GROUP']): return sqlalchemy.true() - creator_filter = (Invite.creator_dn == user.dn) - rolemod_filter = Invite.roles.any(Role.moderator_group_dn.in_(user.group_dns)) + creator_filter = (Invite.creator == user) + rolemod_filter = Invite.roles.any(Role.moderator_group.has(Group.id.in_([group.id for group in user.groups]))) return creator_filter | rolemod_filter def reset_acl_filter(user): if user.is_in_group(current_app.config['ACL_ADMIN_GROUP']): return sqlalchemy.true() - return Invite.creator_dn == user.dn + return Invite.creator == user @bp.route('/') @register_navbar(14, lazy_gettext('Invites'), icon='link', blueprint=bp, visible=invite_acl_check) @@ -58,7 +57,7 @@ def new(): roles = Role.query.all() else: allow_signup = request.user.is_in_group(current_app.config['ACL_SIGNUP_GROUP']) - roles = Role.query.filter(Role.moderator_group_dn.in_(request.user.group_dns)).all() + roles = Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).all() return render_template('invite/new.html', roles=roles, allow_signup=allow_signup) @bp.route('/new', methods=['POST']) @@ -137,7 +136,6 @@ def grant(invite_id, token): if not success: flash(msg) return redirect(url_for('selfservice.index')) - ldap.session.commit() db.session.commit() flash(_('Roles successfully updated')) return redirect(url_for('selfservice.index')) diff --git a/uffd/lazyconfig.py b/uffd/lazyconfig.py deleted file mode 100644 index d31df173..00000000 --- a/uffd/lazyconfig.py +++ /dev/null @@ -1,54 +0,0 @@ -from collections import UserString, UserList - -from flask import current_app - -class LazyConfigString(UserString): - def __init__(self, seq=None, key=None, default=None, error=True): - # pylint: disable=super-init-not-called - self.__seq = seq - self.__key = key - self.__default = default - self.__error = error - - @property - def data(self): - if self.__seq is not None: - obj = self.__seq - elif self.__error: - obj = current_app.config[self.__key] - else: - obj = current_app.config.get(self.__key, self.__default) - return str(obj) - - def __bytes__(self): - return self.data.encode() - - def __get__(self, obj, owner=None): - return self.data - -def lazyconfig_str(key, **kwargs): - return LazyConfigString(None, key, **kwargs) - -class LazyConfigList(UserList): - def __init__(self, seq=None, key=None, default=None, error=True): - # pylint: disable=super-init-not-called - self.__seq = seq - self.__key = key - self.__default = default - self.__error = error - - @property - def data(self): - if self.__seq is not None: - obj = self.__seq - elif self.__error: - obj = current_app.config[self.__key] - else: - obj = current_app.config.get(self.__key, self.__default) - return obj - - def __get__(self, obj, owner=None): - return self.data - -def lazyconfig_list(key, **kwargs): - return LazyConfigList(None, key, **kwargs) diff --git a/uffd/ldap.py b/uffd/ldap.py deleted file mode 100644 index 832ab508..00000000 --- a/uffd/ldap.py +++ /dev/null @@ -1,145 +0,0 @@ -import base64 -import hashlib - -from flask import current_app, request, abort, session - -import ldap3 -from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError, LDAPSASLPrepError - -# We import LDAPCommitError only because it is imported from us by other files. It is not needed here -from uffd.ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import -from uffd.ldapalchemy.model import Query -from uffd.ldapalchemy.core import encode_filter - - -def check_hashed(password_hash, password): - '''Return if password matches a LDAP-compatible password hash (only used for LDAP_SERVICE_MOCK) - - :param password_hash: LDAP-compatible password hash (plain password or "{ssha512}...") - :type password_hash: bytes - :param password: Plain, (ideally) utf8-encoded password - :type password: bytes''' - algorithms = { - b'md5': 'MD5', - b'sha': 'SHA1', - b'sha256': 'SHA256', - b'sha384': 'SHA384', - b'sha512': 'SHA512' - } - if not password_hash.startswith(b'{'): - return password_hash == password - algorithm, data = password_hash[1:].split(b'}', 1) - data = base64.b64decode(data) - if algorithm in algorithms: - ctx = hashlib.new(algorithms[algorithm], password) - return data == ctx.digest() - if algorithm.startswith(b's') and algorithm[1:] in algorithms: - ctx = hashlib.new(algorithms[algorithm[1:]], password) - salt = data[ctx.digest_size:] - ctx.update(salt) - return data == ctx.digest() + salt - raise NotImplementedError() - - -class FlaskQuery(Query): - def get_or_404(self, dn): - res = self.get(dn) - if res is None: - abort(404) - return res - - def first_or_404(self): - res = self.first() - if res is None: - abort(404) - return res - - -def test_user_bind(bind_dn, bind_pw): - try: - if current_app.config.get('LDAP_SERVICE_MOCK', False): - # Since we reuse the same conn and ldap3's mock only supports plain - # passwords for bind and rebind, we simulate the bind by retrieving - # and checking the password hash ourselves. - conn = ldap.get_connection() - conn.search(bind_dn, search_filter='(objectclass=*)', search_scope=ldap3.BASE, - attributes=ldap3.ALL_ATTRIBUTES) - if not conn.response: - return False - if not conn.response[0]['attributes'].get('userPassword'): - return False - return check_hashed(conn.response[0]['attributes']['userPassword'][0], bind_pw.encode()) - - server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"]) - conn = connect_and_bind_to_ldap(server, bind_dn, bind_pw) - if not conn: - return False - except (LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError, LDAPSASLPrepError): - return False - - conn.search(conn.user, encode_filter(current_app.config["LDAP_USER_SEARCH_FILTER"])) - lazy_entries = conn.entries - # Do not end the connection when using mock, as it will be reused afterwards - if not current_app.config.get('LDAP_SERVICE_MOCK', False): - conn.unbind() - return len(lazy_entries) == 1 - - -def connect_and_bind_to_ldap(server, bind_dn, bind_pw): - # Using auto_bind cannot close the connection, so define the connection with extra steps - connection = ldap3.Connection(server, bind_dn, bind_pw) - if connection.closed: - connection.open(read_server_info=False) - if current_app.config["LDAP_SERVICE_USE_STARTTLS"]: - connection.start_tls(read_server_info=False) - if not connection.bind(read_server_info=True): - connection.unbind() - raise LDAPBindError - return connection - - -class FlaskLDAPMapper(LDAPMapper): - def __init__(self): - super().__init__() - - class Model(self.Model): - query_class = FlaskQuery - - self.Model = Model # pylint: disable=invalid-name - - @property - def session(self): - if not hasattr(request, 'ldap_session'): - request.ldap_session = self.Session(self.get_connection) - return request.ldap_session - - def get_connection(self): - if hasattr(request, 'ldap_connection'): - return request.ldap_connection - if current_app.config.get('LDAP_SERVICE_MOCK', False): - if not current_app.debug: - raise Exception('LDAP_SERVICE_MOCK cannot be enabled on production instances') - # Entries are stored in-memory in the mocked `Connection` object. To make - # changes persistent across requests we reuse the same `Connection` object - # for all calls to `service_conn()` and `user_conn()`. - if not hasattr(current_app, 'ldap_mock'): - server = ldap3.Server.from_definition('ldap_mock', 'tests/openldap_mock/ldap_server_info.json', - 'tests/openldap_mock/ldap_server_schema.json') - current_app.ldap_mock = ldap3.Connection(server, client_strategy=ldap3.MOCK_SYNC) - current_app.ldap_mock.strategy.entries_from_json('tests/openldap_mock/ldap_server_entries.json') - current_app.ldap_mock.bind() - return current_app.ldap_mock - server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"], get_info=ldap3.ALL) - - if current_app.config['LDAP_SERVICE_USER_BIND']: - bind_dn = session['user_dn'] - bind_pw = session['user_pw'] - else: - bind_dn = current_app.config["LDAP_SERVICE_BIND_DN"] - bind_pw = current_app.config["LDAP_SERVICE_BIND_PASSWORD"] - - request.ldap_connection = connect_and_bind_to_ldap(server, bind_dn, bind_pw) - return request.ldap_connection - - -ldap = FlaskLDAPMapper() diff --git a/uffd/ldapalchemy/__init__.py b/uffd/ldapalchemy/__init__.py deleted file mode 100644 index 3c0730e8..00000000 --- a/uffd/ldapalchemy/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import ldap3 - -from .core import LDAPCommitError -from . import model, attribute, relationship - -__all__ = ['LDAPMapper', 'LDAPCommitError'] - -class LDAPMapper: - def __init__(self, server=None, bind_dn=None, bind_password=None): - - class Model(model.Model): - ldap_mapper = self - - self.Model = Model # pylint: disable=invalid-name - self.Session = model.Session # pylint: disable=invalid-name - self.Attribute = attribute.Attribute # pylint: disable=invalid-name - self.Relationship = relationship.Relationship # pylint: disable=invalid-name - self.Backreference = relationship.Backreference # pylint: disable=invalid-name - - if not hasattr(type(self), 'server'): - self.server = server - if not hasattr(type(self), 'bind_dn'): - self.bind_dn = bind_dn - if not hasattr(type(self), 'bind_password'): - self.bind_password = bind_password - if not hasattr(type(self), 'session'): - self.session = self.Session(self.get_connection) - - def get_connection(self): - return ldap3.Connection(self.server, self.bind_dn, self.bind_password, auto_bind=True) diff --git a/uffd/ldapalchemy/attribute.py b/uffd/ldapalchemy/attribute.py deleted file mode 100644 index 409cb58a..00000000 --- a/uffd/ldapalchemy/attribute.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections.abc import MutableSequence - -class AttributeList(MutableSequence): - def __init__(self, ldap_object, name, aliases): - self.__ldap_object = ldap_object - self.__name = name - self.__aliases = [name] + aliases - - def __get(self): - return list(self.__ldap_object.getattr(self.__name)) - - def __set(self, values): - for name in self.__aliases: - self.__ldap_object.setattr(name, values) - - def __repr__(self): - return repr(self.__get()) - - def __setitem__(self, key, value): - tmp = self.__get() - tmp[key] = value - self.__set(tmp) - - def __delitem__(self, key): - tmp = self.__get() - del tmp[key] - self.__set(tmp) - - def __len__(self): - return len(self.__get()) - - def __getitem__(self, key): - return self.__get()[key] - - def insert(self, index, value): - tmp = self.__get() - tmp.insert(index, value) - self.__set(tmp) - -class Attribute: - def __init__(self, name, aliases=None, multi=False, default=None): - self.name = name - self.aliases = aliases if aliases is not None else [] - self.multi = multi - self.default = default - - def add_hook(self, obj): - if obj.ldap_object.getattr(self.name) == []: - self.__set__(obj, self.default() if callable(self.default) else self.default) - - def __set_name__(self, cls, name): - if self.default is not None: - cls.ldap_add_hooks = cls.ldap_add_hooks + (self.add_hook,) - - def __get__(self, obj, objtype=None): - if obj is None: - return self - if self.multi: - return AttributeList(obj.ldap_object, self.name, self.aliases) - return (obj.ldap_object.getattr(self.name) or [None])[0] - - def __set__(self, obj, values): - if not self.multi: - values = [values] - for name in [self.name] + self.aliases: - obj.ldap_object.setattr(name, values) diff --git a/uffd/ldapalchemy/core.py b/uffd/ldapalchemy/core.py deleted file mode 100644 index 2d84598f..00000000 --- a/uffd/ldapalchemy/core.py +++ /dev/null @@ -1,284 +0,0 @@ -from ldap3 import MODIFY_REPLACE, MODIFY_DELETE, MODIFY_ADD, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES -from ldap3.utils.conv import escape_filter_chars - -def encode_filter(filter_params): - return '(&%s)'%(''.join(['(%s=%s)'%(attr, escape_filter_chars(value)) for attr, value in filter_params])) - -def match_dn(dn, base): - return dn.endswith(base) # Probably good enougth for all valid dns - -def make_cache_key(search_base, filter_params): - res = (search_base,) - for attr, value in sorted(filter_params): - res += ((attr, value),) - return res - -class LDAPCommitError(Exception): - pass - -class SessionState: - def __init__(self, objects=None, deleted_objects=None, references=None): - self.objects = objects or {} - self.deleted_objects = deleted_objects or {} - self.references = references or {} # {(attr_name, value): {srcobj, ...}, ...} - - def copy(self): - objects = self.objects.copy() - deleted_objects = self.deleted_objects.copy() - references = {key: objs.copy() for key, objs in self.references.items()} - return SessionState(objects, deleted_objects, references) - - def ref(self, obj, attr, values): - for value in values: - key = (attr, value) - if key not in self.references: - self.references[key] = {obj} - else: - self.references[key].add(obj) - - def unref(self, obj, attr, values): - for value in values: - self.references.get((attr, value), set()).discard(obj) - -class ObjectState: - def __init__(self, session=None, attributes=None, dn=None): - self.session = session - self.attributes = attributes or {} - self.dn = dn - - def copy(self): - attributes = {name: values.copy() for name, values in self.attributes.items()} - return ObjectState(attributes=attributes, dn=self.dn, session=self.session) - -class AddOperation: - def __init__(self, obj, dn, object_classes): - self.obj = obj - self.dn = dn - self.object_classes = object_classes - self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()} - - def apply_object(self, obj_state): - obj_state.dn = self.dn - obj_state.attributes = {name: values.copy() for name, values in self.attributes.items()} - obj_state.attributes['objectClass'] = obj_state.attributes.get('objectClass', []) + list(self.object_classes) - - def apply_session(self, session_state): - assert self.dn not in session_state.objects - session_state.objects[self.dn] = self.obj - for name, values in self.attributes.items(): - session_state.ref(self.obj, name, values) - session_state.ref(self.obj, 'objectClass', self.object_classes) - - def apply_ldap(self, conn): - success = conn.add(self.dn, self.object_classes, self.attributes) - if not success: - raise LDAPCommitError() - -class DeleteOperation: - def __init__(self, obj): - self.dn = obj.state.dn - self.obj = obj - self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()} - - def apply_object(self, obj_state): #pylint: disable=no-self-use - obj_state.dn = None - - def apply_session(self, session_state): - assert self.dn in session_state.objects - del session_state.objects[self.dn] - session_state.deleted_objects[self.dn] = self.obj - for name, values in self.attributes.items(): - session_state.unref(self.obj, name, values) - - def apply_ldap(self, conn): - success = conn.delete(self.dn) - if not success: - raise LDAPCommitError() - -class ModifyOperation: - def __init__(self, obj, changes): - self.obj = obj - self.attributes = {name: values.copy() for name, values in obj.state.attributes.items()} - self.changes = changes - - def apply_object(self, obj_state): - for attr, changes in self.changes.items(): - for action, values in changes: - if action == MODIFY_REPLACE: - obj_state.attributes[attr] = values - elif action == MODIFY_ADD: - obj_state.attributes[attr] += values - elif action == MODIFY_DELETE: - for value in values: - if value in obj_state.attributes[attr]: - obj_state.attributes[attr].remove(value) - - def apply_session(self, session_state): - for attr, changes in self.changes.items(): - for action, values in changes: - if action == MODIFY_REPLACE: - session_state.unref(self.obj, attr, self.attributes.get(attr, [])) - session_state.ref(self.obj, attr, values) - elif action == MODIFY_ADD: - session_state.ref(self.obj, attr, values) - elif action == MODIFY_DELETE: - session_state.unref(self.obj, attr, values) - - def apply_ldap(self, conn): - success = conn.modify(self.obj.state.dn, self.changes) - if not success: - raise LDAPCommitError() - -class Session: - def __init__(self, get_connection): - self.get_connection = get_connection - self.committed_state = SessionState() - self.state = SessionState() - self.changes = [] - self.cached_searches = set() - - def add(self, obj, dn, object_classes): - if self.state.objects.get(dn) == obj: - return - assert obj.state.session is None - oper = AddOperation(obj, dn, object_classes) - oper.apply_object(obj.state) - obj.state.session = self - oper.apply_session(self.state) - self.changes.append(oper) - - def delete(self, obj): - if obj.state.dn not in self.state.objects: - return - assert obj.state.session == self - oper = DeleteOperation(obj) - oper.apply_object(obj.state) - obj.state.session = None - oper.apply_session(self.state) - self.changes.append(oper) - - def record(self, oper): - assert oper.obj.state.session == self - self.changes.append(oper) - - def commit(self): - conn = self.get_connection() - while self.changes: - oper = self.changes.pop(0) - try: - oper.apply_ldap(conn) - except Exception as err: - self.changes.insert(0, oper) - raise err - oper.apply_object(oper.obj.committed_state) - oper.apply_session(self.committed_state) - self.committed_state = self.state.copy() - - def rollback(self): - for obj in self.state.objects.values(): - obj.state = obj.committed_state.copy() - for obj in self.state.deleted_objects.values(): - obj.state = obj.committed_state.copy() - self.state = self.committed_state.copy() - self.changes.clear() - - def get(self, dn, filter_params): - if dn in self.state.objects: - obj = self.state.objects[dn] - return obj if obj.match(filter_params) else None - if dn in self.state.deleted_objects: - return None - conn = self.get_connection() - conn.search(dn, encode_filter(filter_params), attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES]) - if not conn.response: - return None - assert len(conn.response) == 1 - if conn.response[0]['dn'] != dn: - # To use DNs as cache keys, we assume each DN has a single unique string - # representation. This is not generally true: RDN attributes may be - # case insensitive or values may contain escape sequences. - # In this case, the provided DN differs from the canonical form the - # server returned. We cannot handle this consistently, so we report no - # match. - return None - obj = Object(self, conn.response[0]) - self.state.objects[dn] = obj - self.committed_state.objects[dn] = obj - for attr, values in obj.state.attributes.items(): - self.state.ref(obj, attr, values) - return obj - - def filter(self, search_base, filter_params): - if not filter_params: - matches = self.state.objects.values() - else: - submatches = [self.state.references.get((attr, value), set()) for attr, value in filter_params] - matches = submatches.pop(0) - while submatches: - matches = matches.intersection(submatches.pop(0)) - res = [obj for obj in matches if match_dn(obj.state.dn, search_base)] - cache_key = make_cache_key(search_base, filter_params) - if cache_key in self.cached_searches: - return res - conn = self.get_connection() - conn.search(search_base, encode_filter(filter_params), attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES]) - for response in conn.response: - dn = response['dn'] - if dn in self.state.objects or dn in self.state.deleted_objects: - continue - obj = Object(self, response) - self.state.objects[dn] = obj - self.committed_state.objects[dn] = obj - for attr, values in obj.state.attributes.items(): - self.state.ref(obj, attr, values) - res.append(obj) - self.cached_searches.add(cache_key) - return res - -class Object: - def __init__(self, session=None, response=None): - if response is None: - self.committed_state = ObjectState() - else: - assert session is not None - attrs = {attr: value if isinstance(value, list) else [value] for attr, value in response['attributes'].items()} - self.committed_state = ObjectState(session, attrs, response['dn']) - self.state = self.committed_state.copy() - - @property - def dn(self): - return self.state.dn - - @property - def session(self): - return self.state.session - - def getattr(self, name): - return self.state.attributes.get(name, []) - - def setattr(self, name, values): - oper = ModifyOperation(self, {name: [(MODIFY_REPLACE, values)]}) - oper.apply_object(self.state) - if self.state.session: - oper.apply_session(self.state.session.state) - self.state.session.changes.append(oper) - - def attr_append(self, name, value): - oper = ModifyOperation(self, {name: [(MODIFY_ADD, [value])]}) - oper.apply_object(self.state) - if self.state.session: - oper.apply_session(self.state.session.state) - self.state.session.changes.append(oper) - - def attr_remove(self, name, value): - oper = ModifyOperation(self, {name: [(MODIFY_DELETE, [value])]}) - oper.apply_object(self.state) - if self.state.session: - oper.apply_session(self.state.session.state) - self.state.session.changes.append(oper) - - def match(self, filter_params): - for attr, value in filter_params: - if value not in self.getattr(attr): - return False - return True diff --git a/uffd/ldapalchemy/dbutils.py b/uffd/ldapalchemy/dbutils.py deleted file mode 100644 index dfc93f9a..00000000 --- a/uffd/ldapalchemy/dbutils.py +++ /dev/null @@ -1,145 +0,0 @@ -from collections.abc import MutableSet - -from .model import add_to_session - -class DBRelationshipSet(MutableSet): - def __init__(self, dbobj, relattr, ldapcls, mapcls): - self.__dbobj = dbobj - self.__relattr = relattr - self.__ldapcls = ldapcls - self.__mapcls = mapcls - - def __get_dns(self): - return [mapobj.dn for mapobj in getattr(self.__dbobj, self.__relattr)] - - def __repr__(self): - return repr(set(self)) - - def __contains__(self, value): - if value is None or not isinstance(value, self.__ldapcls): - return False - return value.ldap_object.dn in self.__get_dns() - - def __iter__(self): - return iter(filter(lambda obj: obj is not None, [self.__ldapcls.query.get(dn) for dn in self.__get_dns()])) - - def __len__(self): - return len(set(self)) - - def add(self, value): - if not isinstance(value, self.__ldapcls): - raise TypeError() - if value.ldap_object.session is None: - add_to_session(value, self.__ldapcls.ldap_mapper.session.ldap_session) - if value.ldap_object.dn not in self.__get_dns(): - getattr(self.__dbobj, self.__relattr).append(self.__mapcls(dn=value.ldap_object.dn)) - - def discard(self, value): - if not isinstance(value, self.__ldapcls): - raise TypeError() - rel = getattr(self.__dbobj, self.__relattr) - for mapobj in list(rel): - if mapobj.dn == value.ldap_object.dn: - rel.remove(mapobj) - -class DBRelationship: - def __init__(self, relattr, ldapcls, mapcls=None, backref=None, backattr=None): - self.relattr = relattr - self.ldapcls = ldapcls - self.mapcls = mapcls - self.backref = backref - self.backattr = backattr - - def __set_name__(self, cls, name): - if self.backref: - setattr(self.ldapcls, self.backref, DBBackreference(cls, self.relattr, self.mapcls, self.backattr)) - - def __get__(self, obj, objtype=None): - if obj is None: - return self - if self.mapcls is not None: - return DBRelationshipSet(obj, self.relattr, self.ldapcls, self.mapcls) - dn = getattr(obj, self.relattr) - if dn is not None: - return self.ldapcls.query.get(dn) - return None - - def __set__(self, obj, values): - if self.mapcls is not None: - tmp = self.__get__(obj) - tmp.clear() - for value in values: - tmp.add(value) - else: - if not isinstance(values, self.ldapcls): - raise TypeError() - setattr(obj, self.relattr, values.ldap_object.dn) - -class DBBackreferenceSet(MutableSet): - def __init__(self, ldapobj, dbcls, relattr, mapcls, backattr): - self.__ldapobj = ldapobj - self.__dbcls = dbcls - self.__relattr = relattr - self.__mapcls = mapcls - self.__backattr = backattr - - @property - def __dn(self): - return self.__ldapobj.ldap_object.dn - - def __get(self): - if self.__mapcls is None: - return self.__dbcls.query.filter_by(**{self.__relattr: self.__dn}).all() - return {getattr(mapobj, self.__backattr) for mapobj in self.__mapcls.query.filter_by(dn=self.__dn)} - - def __repr__(self): - return repr(self.__get()) - - def __contains__(self, value): - return value in self.__get() - - def __iter__(self): - return iter(self.__get()) - - def __len__(self): - return len(self.__get()) - - def add(self, value): - assert self.__ldapobj.ldap_object.session is not None - if not isinstance(value, self.__dbcls): - raise TypeError() - if self.__mapcls is None: - setattr(value, self.__relattr, self.__dn) - else: - rel = getattr(value, self.__relattr) - if self.__dn not in {mapobj.dn for mapobj in rel}: - rel.append(self.__mapcls(dn=self.__dn)) - - def discard(self, value): - if not isinstance(value, self.__dbcls): - raise TypeError() - if self.__mapcls is None: - setattr(value, self.__relattr, None) - else: - rel = getattr(value, self.__relattr) - for mapobj in list(rel): - if mapobj.dn == self.__dn: - rel.remove(mapobj) - -class DBBackreference: - def __init__(self, dbcls, relattr, mapcls=None, backattr=None): - self.dbcls = dbcls - self.relattr = relattr - self.mapcls = mapcls - self.backattr = backattr - - def __get__(self, obj, objtype=None): - if obj is None: - return self - return DBBackreferenceSet(obj, self.dbcls, self.relattr, self.mapcls, self.backattr) - - def __set__(self, obj, values): - tmp = self.__get__(obj) - tmp.clear() - for value in values: - tmp.add(value) diff --git a/uffd/ldapalchemy/model.py b/uffd/ldapalchemy/model.py deleted file mode 100644 index c5e2fc33..00000000 --- a/uffd/ldapalchemy/model.py +++ /dev/null @@ -1,148 +0,0 @@ -from collections.abc import Sequence - -try: - # Added in v2.5 - from ldap3.utils.dn import escape_rdn -except ImportError: - # From ldap3 source code, Copyright Giovanni Cannata, LGPL v3 license - def escape_rdn(rdn): - # '/' must be handled first or the escape slashes will be escaped! - for char in ['\\', ',', '+', '"', '<', '>', ';', '=', '\x00']: - rdn = rdn.replace(char, '\\' + char) - if rdn[0] == '#' or rdn[0] == ' ': - rdn = ''.join(('\\', rdn)) - if rdn[-1] == ' ': - rdn = ''.join((rdn[:-1], '\\ ')) - return rdn - -from . import core - -def add_to_session(obj, session): - if obj.ldap_object.session is None: - for func in obj.ldap_add_hooks: - func(obj) - session.add(obj.ldap_object, obj.dn, obj.ldap_object_classes) - -class Session: - def __init__(self, get_connection): - self.ldap_session = core.Session(get_connection) - - def add(self, obj): - add_to_session(obj, self.ldap_session) - - def delete(self, obj): - self.ldap_session.delete(obj.ldap_object) - - def commit(self): - self.ldap_session.commit() - - def rollback(self): - self.ldap_session.rollback() - -def make_modelobj(obj, model): - if obj is None: - return None - if not hasattr(obj, 'model'): - obj.model = model() - obj.model.ldap_object = obj - if not isinstance(obj.model, model): - return None - return obj.model - -def make_modelobjs(objs, model): - modelobjs = [] - for obj in objs: - modelobj = make_modelobj(obj, model) - if modelobj is not None: - modelobjs.append(modelobj) - return modelobjs - -class Query(Sequence): - def __init__(self, model, filter_params=None): - self.__model = model - self.__filter_params = list(model.ldap_filter_params) + (filter_params or []) - - @property - def __session(self): - return self.__model.ldap_mapper.session.ldap_session - - def get(self, dn): - return make_modelobj(self.__session.get(dn, self.__filter_params), self.__model) - - def all(self): - objs = self.__session.filter(self.__model.ldap_search_base, self.__filter_params) - objs = sorted(objs, key=lambda obj: obj.dn) - return make_modelobjs(objs, self.__model) - - def first(self): - return (self.all() or [None])[0] - - def one(self): - modelobjs = self.all() - if len(modelobjs) != 1: - raise Exception() - return modelobjs[0] - - def one_or_none(self): - modelobjs = self.all() - if len(modelobjs) > 1: - raise Exception() - return (modelobjs or [None])[0] - - def __contains__(self, value): - return value in self.all() - - def __iter__(self): - return iter(self.all()) - - def __len__(self): - return len(self.all()) - - def __getitem__(self, index): - return self.all()[index] - - def filter_by(self, **kwargs): - filter_params = [(getattr(self.__model, attr).name, value) for attr, value in kwargs.items()] - return type(self)(self.__model, self.__filter_params + filter_params) - -class QueryWrapper: - def __get__(self, obj, objtype=None): - return objtype.query_class(objtype) - -class Model: - # Overwritten by mapper - ldap_mapper = None - query_class = Query - query = QueryWrapper() - ldap_add_hooks = () - - # Overwritten by models - ldap_search_base = None - ldap_filter_params = () - ldap_object_classes = () - ldap_dn_base = None - ldap_dn_attribute = None - - def __init__(self, **kwargs): - self.ldap_object = core.Object() - for key, value, in kwargs.items(): - setattr(self, key, value) - - @property - def dn(self): - if self.ldap_object.dn is not None: - return self.ldap_object.dn - if self.ldap_dn_base is None or self.ldap_dn_attribute is None: - return None - values = self.ldap_object.getattr(self.ldap_dn_attribute) - if not values: - return None - # escape_rdn can't handle empty strings - rdn = escape_rdn(values[0]) if values[0] else '' - return '%s=%s,%s'%(self.ldap_dn_attribute, rdn, self.ldap_dn_base) - - def __repr__(self): - cls_name = '%s.%s'%(type(self).__module__, type(self).__name__) - if self.dn is not None: - return '<%s %s>'%(cls_name, self.dn) - return '<%s>'%cls_name diff --git a/uffd/ldapalchemy/relationship.py b/uffd/ldapalchemy/relationship.py deleted file mode 100644 index 5666a5d3..00000000 --- a/uffd/ldapalchemy/relationship.py +++ /dev/null @@ -1,136 +0,0 @@ -from collections.abc import MutableSet - -from .model import make_modelobj, make_modelobjs, add_to_session - -class UnboundObjectError(Exception): - pass - -class RelationshipSet(MutableSet): - def __init__(self, ldap_object, name, model, destmodel): - self.__ldap_object = ldap_object - self.__name = name - self.__model = model # pylint: disable=unused-private-member - self.__destmodel = destmodel - - def __modify_check(self, value): - if self.__ldap_object.session is None: - raise UnboundObjectError() - if not isinstance(value, self.__destmodel): - raise TypeError() - - def __repr__(self): - return repr(set(self)) - - def __contains__(self, value): - if value is None or not isinstance(value, self.__destmodel): - return False - return value.ldap_object.dn in self.__ldap_object.getattr(self.__name) - - def __iter__(self): - def get(dn): - return make_modelobj(self.__ldap_object.session.get(dn, self.__destmodel.ldap_filter_params), self.__destmodel) - dns = set(self.__ldap_object.getattr(self.__name)) - return iter(filter(lambda obj: obj is not None, map(get, dns))) - - def __len__(self): - return len(set(self)) - - def add(self, value): - self.__modify_check(value) - if value.ldap_object.session is None: - add_to_session(value, self.__ldap_object.session) - assert value.ldap_object.session == self.__ldap_object.session - self.__ldap_object.attr_append(self.__name, value.dn) - - def discard(self, value): - self.__modify_check(value) - self.__ldap_object.attr_remove(self.__name, value.dn) - - def update(self, values): - for value in values: - self.add(value) - -class Relationship: - def __init__(self, name, destmodel, backref=None): - self.name = name - self.destmodel = destmodel - self.backref = backref - - def __set_name__(self, cls, name): - if self.backref is not None: - setattr(self.destmodel, self.backref, Backreference(self.name, cls)) - - def __get__(self, obj, objtype=None): - if obj is None: - return self - return RelationshipSet(obj.ldap_object, self.name, type(obj), self.destmodel) - - def __set__(self, obj, values): - tmp = self.__get__(obj) - tmp.clear() - for value in values: - tmp.add(value) - -class BackreferenceSet(MutableSet): - def __init__(self, ldap_object, name, model, srcmodel): - self.__ldap_object = ldap_object - self.__name = name - self.__model = model # pylint: disable=unused-private-member - self.__srcmodel = srcmodel - - def __modify_check(self, value): - if self.__ldap_object.session is None: - raise UnboundObjectError() - if not isinstance(value, self.__srcmodel): - raise TypeError() - - def __get(self): - if self.__ldap_object.session is None: - return set() - filter_params = list(self.__srcmodel.ldap_filter_params) + [(self.__name, self.__ldap_object.dn)] - objs = self.__ldap_object.session.filter(self.__srcmodel.ldap_search_base, filter_params) - return set(make_modelobjs(objs, self.__srcmodel)) - - def __repr__(self): - return repr(self.__get()) - - def __contains__(self, value): - return value in self.__get() - - def __iter__(self): - return iter(self.__get()) - - def __len__(self): - return len(self.__get()) - - def add(self, value): - self.__modify_check(value) - if value.ldap_object.session is None: - add_to_session(value, self.__ldap_object.session) - assert value.ldap_object.session == self.__ldap_object.session - if self.__ldap_object.dn not in value.ldap_object.getattr(self.__name): - value.ldap_object.attr_append(self.__name, self.__ldap_object.dn) - - def discard(self, value): - self.__modify_check(value) - value.ldap_object.attr_remove(self.__name, self.__ldap_object.dn) - - def update(self, values): - for value in values: - self.add(value) - -class Backreference: - def __init__(self, name, srcmodel): - self.name = name - self.srcmodel = srcmodel - - def __get__(self, obj, objtype=None): - if obj is None: - return self - return BackreferenceSet(obj.ldap_object, self.name, type(obj), self.srcmodel) - - def __set__(self, obj, values): - tmp = self.__get__(obj) - tmp.clear() - for value in values: - tmp.add(value) diff --git a/uffd/mail/models.py b/uffd/mail/models.py index 1791170a..7734ec97 100644 --- a/uffd/mail/models.py +++ b/uffd/mail/models.py @@ -1,13 +1,32 @@ -from uffd.ldap import ldap -from uffd.lazyconfig import lazyconfig_str, lazyconfig_list - -class Mail(ldap.Model): - ldap_search_base = lazyconfig_str('LDAP_MAIL_SEARCH_BASE') - ldap_filter_params = lazyconfig_list('LDAP_MAIL_SEARCH_FILTER') - ldap_object_classes = lazyconfig_list('LDAP_MAIL_OBJECTCLASSES') - ldap_dn_attribute = lazyconfig_str('LDAP_MAIL_DN_ATTRIBUTE') - ldap_dn_base = lazyconfig_str('LDAP_MAIL_SEARCH_BASE') - - uid = ldap.Attribute(lazyconfig_str('LDAP_MAIL_UID_ATTRIBUTE')) - receivers = ldap.Attribute(lazyconfig_str('LDAP_MAIL_RECEIVERS_ATTRIBUTE'), multi=True) - destinations = ldap.Attribute(lazyconfig_str('LDAP_MAIL_DESTINATIONS_ATTRIBUTE'), multi=True) +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.ext.associationproxy import association_proxy + +from uffd.database import db + +class Mail(db.Model): + __tablename__ = 'mail' + id = Column(Integer(), primary_key=True, autoincrement=True) + uid = Column(String(32), unique=True, nullable=False) + _receivers = relationship('MailReceiveAddress', cascade='all, delete-orphan') + receivers = association_proxy('_receivers', 'address') + _destinations = relationship('MailDestinationAddress', cascade='all, delete-orphan') + destinations = association_proxy('_destinations', 'address') + +class MailReceiveAddress(db.Model): + __tablename__ = 'mail_receive_address' + id = Column(Integer(), primary_key=True, autoincrement=True) + mail_id = Column(Integer(), ForeignKey('mail.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + address = Column(String(128), nullable=False) + + def __init__(self, address): + self.address = address + +class MailDestinationAddress(db.Model): + __tablename__ = 'mail_destination_address' + id = Column(Integer(), primary_key=True, autoincrement=True) + mail_id = Column(Integer(), ForeignKey('mail.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + address = Column(String(128), nullable=False) + + def __init__(self, address): + self.address = address diff --git a/uffd/mail/views.py b/uffd/mail/views.py index 9f5f2e1d..47af1a84 100644 --- a/uffd/mail/views.py +++ b/uffd/mail/views.py @@ -3,7 +3,7 @@ from flask_babel import gettext as _, lazy_gettext from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.ldap import ldap +from uffd.database import db from uffd.session import login_required from uffd.mail.models import Mail @@ -41,8 +41,8 @@ def update(uid=None): mail = Mail(uid=request.form.get('mail-uid')) mail.receivers = request.form.get('mail-receivers', '').splitlines() mail.destinations = request.form.get('mail-destinations', '').splitlines() - ldap.session.add(mail) - ldap.session.commit() + db.session.add(mail) + db.session.commit() flash(_('Mail mapping updated.')) return redirect(url_for('mail.show', uid=mail.uid)) @@ -50,7 +50,7 @@ def update(uid=None): @csrf_protect(blueprint=bp) def delete(uid): mail = Mail.query.filter_by(uid=uid).first_or_404() - ldap.session.delete(mail) - ldap.session.commit() + db.session.delete(mail) + db.session.commit() flash(_('Deleted mail mapping.')) return redirect(url_for('mail.index')) diff --git a/uffd/mfa/models.py b/uffd/mfa/models.py index cc970df7..81b6abd7 100644 --- a/uffd/mfa/models.py +++ b/uffd/mfa/models.py @@ -12,9 +12,9 @@ import urllib.parse import crypt from flask import request, current_app -from sqlalchemy import Column, Integer, Enum, String, DateTime, Text +from sqlalchemy import Column, Integer, Enum, String, DateTime, Text, ForeignKey +from sqlalchemy.orm import relationship, backref -from uffd.ldapalchemy.dbutils import DBRelationship from uffd.database import db from uffd.user.models import User @@ -28,11 +28,11 @@ class MFAType(enum.Enum): class MFAMethod(db.Model): __tablename__ = 'mfa_method' id = Column(Integer(), primary_key=True, autoincrement=True) - type = Column(Enum(MFAType)) - created = Column(DateTime()) + type = Column(Enum(MFAType), nullable=False) + created = Column(DateTime(), nullable=False, default=datetime.datetime.now) name = Column(String(128)) - dn = Column(String(128)) - user = DBRelationship('dn', User, backref='mfa_methods') + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + user = relationship('User', backref=backref('mfa_methods', cascade='all, delete-orphan')) __mapper_args__ = { 'polymorphic_on': type, @@ -46,7 +46,7 @@ class MFAMethod(db.Model): class RecoveryCodeMethod(MFAMethod): code_salt = Column('recovery_salt', String(64)) code_hash = Column('recovery_hash', String(256)) - user = DBRelationship('dn', User, backref='mfa_recovery_codes') + user = relationship('User', backref='mfa_recovery_codes') __mapper_args__ = { 'polymorphic_identity': MFAType.RECOVERY_CODE @@ -79,7 +79,7 @@ def _hotp(counter, key, digits=6): class TOTPMethod(MFAMethod): key = Column('totp_key', String(64)) - user = DBRelationship('dn', User, backref='mfa_totp_methods') + user = relationship('User', backref='mfa_totp_methods') __mapper_args__ = { 'polymorphic_identity': MFAType.TOTP @@ -129,7 +129,7 @@ class TOTPMethod(MFAMethod): class WebauthnMethod(MFAMethod): _cred = Column('webauthn_cred', Text()) - user = DBRelationship('dn', User, backref='mfa_webauthn_methods') + user = relationship('User', backref='mfa_webauthn_methods') __mapper_args__ = { 'polymorphic_identity': MFAType.WEBAUTHN diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py index 4c820460..60f0b472 100644 --- a/uffd/mfa/views.py +++ b/uffd/mfa/views.py @@ -5,7 +5,6 @@ from flask import Blueprint, render_template, session, request, redirect, url_fo from flask_babel import gettext as _ from uffd.database import db -from uffd.ldap import ldap from uffd.mfa.models import MFAMethod, TOTPMethod, WebauthnMethod, RecoveryCodeMethod from uffd.session.views import login_required, login_required_pre_mfa, set_request_user from uffd.user.models import User @@ -31,33 +30,32 @@ def disable(): @login_required() @csrf_protect(blueprint=bp) def disable_confirm(): - MFAMethod.query.filter_by(dn=request.user.dn).delete() + MFAMethod.query.filter_by(user=request.user).delete() db.session.commit() request.user.update_groups() - ldap.session.commit() + db.session.commit() return redirect(url_for('mfa.setup')) -@bp.route('/admin/<int:uid>/disable') +@bp.route('/admin/<int:id>/disable') @login_required() @csrf_protect(blueprint=bp) -def admin_disable(uid): +def admin_disable(id): # Group cannot be checked with login_required kwarg, because the config # variable is not available when the decorator is processed if not request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']): abort(403) - user = User.query.filter_by(uid=uid).one() - MFAMethod.query.filter_by(dn=user.dn).delete() - db.session.commit() + user = User.query.get(id) + MFAMethod.query.filter_by(user=user).delete() user.update_groups() - ldap.session.commit() + db.session.commit() flash(_('Two-factor authentication was reset')) - return redirect(url_for('user.show', uid=uid)) + return redirect(url_for('user.show', id=id)) @bp.route('/setup/recovery', methods=['POST']) @login_required() @csrf_protect(blueprint=bp) def setup_recovery(): - for method in RecoveryCodeMethod.query.filter_by(dn=request.user.dn).all(): + for method in RecoveryCodeMethod.query.filter_by(user=request.user).all(): db.session.delete(method) methods = [] for _ in range(10): @@ -78,15 +76,14 @@ def setup_totp(): @login_required() @csrf_protect(blueprint=bp) def setup_totp_finish(): - if not RecoveryCodeMethod.query.filter_by(dn=request.user.dn).all(): + if not RecoveryCodeMethod.query.filter_by(user=request.user).all(): flash(_('Generate recovery codes first!')) return redirect(url_for('mfa.setup')) method = TOTPMethod(request.user, name=request.values['name'], key=session.pop('mfa_totp_key')) if method.verify(request.form['code']): db.session.add(method) - db.session.commit() request.user.update_groups() - ldap.session.commit() + db.session.commit() return redirect(url_for('mfa.setup')) flash(_('Code is invalid')) return redirect(url_for('mfa.setup_totp', name=request.values['name'])) @@ -95,11 +92,10 @@ def setup_totp_finish(): @login_required() @csrf_protect(blueprint=bp) def delete_totp(id): #pylint: disable=redefined-builtin - method = TOTPMethod.query.filter_by(dn=request.user.dn, id=id).first_or_404() + method = TOTPMethod.query.filter_by(user=request.user, id=id).first_or_404() db.session.delete(method) - db.session.commit() request.user.update_groups() - ldap.session.commit() + db.session.commit() return redirect(url_for('mfa.setup')) # WebAuthn support is optional because fido2 has a pretty unstable @@ -139,14 +135,14 @@ if WEBAUTHN_SUPPORTED: @login_required() @csrf_protect(blueprint=bp) def setup_webauthn_begin(): - if not RecoveryCodeMethod.query.filter_by(dn=request.user.dn).all(): + if not RecoveryCodeMethod.query.filter_by(user=request.user).all(): abort(403) - methods = WebauthnMethod.query.filter_by(dn=request.user.dn).all() + methods = WebauthnMethod.query.filter_by(user=request.user).all() creds = [method.cred for method in methods] server = get_webauthn_server() registration_data, state = server.register_begin( { - "id": request.user.dn.encode(), + "id": str(request.user.id).encode(), "name": request.user.loginname, "displayName": request.user.displayname, }, @@ -167,9 +163,8 @@ if WEBAUTHN_SUPPORTED: auth_data = server.register_complete(session["webauthn-state"], client_data, att_obj) method = WebauthnMethod(request.user, auth_data.credential_data, name=data['name']) db.session.add(method) - db.session.commit() request.user.update_groups() - ldap.session.commit() + db.session.commit() return cbor.encode({"status": "OK"}) @bp.route("/auth/webauthn/begin", methods=["POST"]) @@ -213,11 +208,10 @@ if WEBAUTHN_SUPPORTED: @login_required() @csrf_protect(blueprint=bp) def delete_webauthn(id): #pylint: disable=redefined-builtin - method = WebauthnMethod.query.filter_by(dn=request.user.dn, id=id).first_or_404() + method = WebauthnMethod.query.filter_by(user=request.user, id=id).first_or_404() db.session.delete(method) - db.session.commit() request.user.update_groups() - ldap.session.commit() + db.session.commit() return redirect(url_for('mfa.setup')) @bp.route('/auth', methods=['GET']) @@ -233,7 +227,7 @@ def auth(): @bp.route('/auth', methods=['POST']) @login_required_pre_mfa() def auth_finish(): - delay = mfa_ratelimit.get_delay(request.user_pre_mfa.dn) + delay = mfa_ratelimit.get_delay(request.user_pre_mfa.id) if delay: flash(_('We received too many invalid attempts! Please wait at least %s.')%format_delay(delay)) return redirect(url_for('mfa.auth', ref=request.values.get('ref'))) @@ -255,6 +249,6 @@ def auth_finish(): flash(_('You only have a few recovery codes remaining. Make sure to generate new ones before they run out.')) return redirect(url_for('mfa.setup')) return secure_local_redirect(request.values.get('ref', url_for('index'))) - mfa_ratelimit.log(request.user_pre_mfa.dn) + mfa_ratelimit.log(request.user_pre_mfa.id) flash(_('Two-factor authentication failed')) return redirect(url_for('mfa.auth', ref=request.values.get('ref'))) diff --git a/uffd/migrations/versions/878b25c4fae7_ldap_to_db.py b/uffd/migrations/versions/878b25c4fae7_ldap_to_db.py new file mode 100644 index 00000000..b03c059c --- /dev/null +++ b/uffd/migrations/versions/878b25c4fae7_ldap_to_db.py @@ -0,0 +1,956 @@ +"""LDAP to DB + +Revision ID: 878b25c4fae7 +Revises: 11ecc8f1ac3b +Create Date: 2021-08-01 16:31:09.242380 + +""" +from warnings import warn + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '878b25c4fae7' +down_revision = '11ecc8f1ac3b' +branch_labels = None +depends_on = None + +from flask import current_app + +def encode_filter(filter_params): + from ldap3.utils.conv import escape_filter_chars + return '(&%s)'%(''.join(['(%s=%s)'%(attr, escape_filter_chars(value)) for attr, value in filter_params])) + +def get_ldap_conn(): + critical = True + if 'LDAP_SERVICE_URL' not in current_app.config: + critical = False + try: + if current_app.config.get('LDAP_SERVICE_USER_BIND'): + raise Exception('Import with LDAP_SERVICE_USER_BIND=True is not supported') + if current_app.config.get('LDAP_SERVICE_MOCK'): + # never reached if current_app.testing is True + raise Exception('Import with LDAP_SERVICE_MOCK=True is not supported') + import ldap3 + server = ldap3.Server(current_app.config.get('LDAP_SERVICE_URL', 'ldapi:///'), get_info=ldap3.ALL) + # Using auto_bind cannot close the connection, so define the connection with extra steps + conn = ldap3.Connection(server, current_app.config.get('LDAP_SERVICE_BIND_DN', ''), + current_app.config.get('LDAP_SERVICE_BIND_PASSWORD', '')) + if conn.closed: + conn.open(read_server_info=False) + if current_app.config.get('LDAP_SERVICE_USE_STARTTLS', True): + conn.start_tls(read_server_info=False) + if not conn.bind(read_server_info=True): + conn.unbind() + raise ldap3.core.exceptions.LDAPBindError + return conn + except Exception as e: + if critical: + raise e + else: + warn(f'LDAP not properly configured, disabling import: {e}') + return None + +def get_ldap_users(): + if current_app.config.get('LDAP_SERVICE_MOCK') and current_app.testing: + return [ + {'dn': 'uid=testuser,ou=users,dc=example,dc=com', 'unix_uid': 10000, 'loginname': 'testuser', + 'displayname': 'Test User', 'mail': 'testuser@example.com', + 'pwhash': '{ssha512}llnQc2ruKczLUHJUPA3/MGA1rkChXcmYdIeMRfKC8NfsqnHTtd2UmSZ7RL4uTExzAcMyYKxLwyjmjZfycjLHBjR6NJeK1sz3', + 'is_service_user': False}, + {'dn': 'uid=testadmin,ou=users,dc=example,dc=com', 'unix_uid': 10001, 'loginname': 'testadmin', + 'displayname': 'Test Admin', 'mail': 'testadmin@example.com', + 'pwhash': '{ssha512}8pI4sHQWEgDf9u4qj35QT3J1lskLrnWdvhlzSmYg1g2R1r/038f6we+8Hy5ld/KArApB9Gd9+4uitKbZVbR1CkuPT2iAWoMc', + 'is_service_user': False}, + ] + conn = get_ldap_conn() + if not conn: + return [] + conn.search(current_app.config.get('LDAP_USER_SEARCH_BASE', 'ou=users,dc=example,dc=com'), + encode_filter(current_app.config.get('LDAP_USER_SEARCH_FILTER', [('objectClass', 'person')])), + attributes='*') + users = [] + for response in conn.response: + uid = response['attributes'][current_app.config.get('LDAP_USER_UID_ATTRIBUTE', 'uidNumber')] + pwhash = response['attributes'].get('userPassword', [None])[0] + if pwhash is None: + raise Exception('Cannot read userPassword attribute') + elif isinstance(pwhash, bytes): + pwhash = pwhash.decode() + users.append({ + 'dn': response['dn'], + 'unix_uid': uid, + 'loginname': response['attributes'][current_app.config.get('LDAP_USER_LOGINNAME_ATTRIBUTE', 'uid')][0], + 'displayname': response['attributes'].get(current_app.config.get('LDAP_USER_DISPLAYNAME_ATTRIBUTE', 'cn'), [''])[0], + 'mail': response['attributes'][current_app.config.get('LDAP_USER_MAIL_ATTRIBUTE', 'mail')][0], + 'pwhash': pwhash, + 'is_service_user': uid >= current_app.config.get('LDAP_USER_SERVICE_MIN_UID', 19000) and \ + uid <= current_app.config.get('LDAP_USER_SERVICE_MAX_UID', 19999), + }) + return users + +def get_ldap_groups(): + if current_app.config.get('LDAP_SERVICE_MOCK') and current_app.testing: + return [ + {'dn': 'cn=users,ou=groups,dc=example,dc=com', 'unix_gid': 20001, 'name': 'users', + 'description': 'Base group for all users', 'member_dns': ['cn=dummy,ou=system,dc=example,dc=com', + 'uid=testuser,ou=users,dc=example,dc=com', + 'uid=testadmin,ou=users,dc=example,dc=com']}, + {'dn': 'cn=uffd_access,ou=groups,dc=example,dc=com', 'unix_gid': 20002, 'name': 'uffd_access', + 'description': 'User access to uffd selfservice', 'member_dns': ['cn=dummy,ou=system,dc=example,dc=com', + 'uid=testuser,ou=users,dc=example,dc=com', + 'uid=testadmin,ou=users,dc=example,dc=com']}, + {'dn': 'cn=uffd_admin,ou=groups,dc=example,dc=com', 'unix_gid': 20003, 'name': 'uffd_admin', + 'description': 'User access to uffd selfservice', 'member_dns': ['cn=dummy,ou=system,dc=example,dc=com', + 'uid=testadmin,ou=users,dc=example,dc=com']}, + ] + conn = get_ldap_conn() + if not conn: + return [] + conn.search(current_app.config.get('LDAP_GROUP_SEARCH_BASE', 'ou=groups,dc=example,dc=com'), + encode_filter(current_app.config.get('LDAP_GROUP_SEARCH_FILTER', [('objectClass','groupOfUniqueNames')])), + attributes='*') + groups = [] + for response in conn.response: + groups.append({ + 'dn': response['dn'], + 'unix_gid': response['attributes'][current_app.config.get('LDAP_GROUP_GID_ATTRIBUTE', 'gidNumber')], + 'name': response['attributes'][current_app.config.get('LDAP_GROUP_NAME_ATTRIBUTE', 'cn')][0], + 'description': response['attributes'].get(current_app.config.get('LDAP_GROUP_DESCRIPTION_ATTRIBUTE', 'description'), [''])[0], + 'member_dns': response['attributes'].get(current_app.config.get('LDAP_GROUP_MEMBER_ATTRIBUTE', 'uniqueMember'), []), + }) + return groups + +def get_ldap_mails(): + if current_app.config.get('LDAP_SERVICE_MOCK') and current_app.testing: + return [ + {'dn': 'uid=test,ou=postfix,dc=example,dc=com', 'uid': 'test', + 'receivers': ['test1@example.com', 'test2@example.com'], + 'destinations': ['testuser@mail.example.com']}, + ] + conn = get_ldap_conn() + if not conn: + return [] + conn.search(current_app.config.get('LDAP_MAIL_SEARCH_BASE', 'ou=postfix,dc=example,dc=com'), + encode_filter(current_app.config.get('LDAP_MAIL_SEARCH_FILTER', [('objectClass','postfixVirtual')])), + attributes='*') + mails = [] + for response in conn.response: + mails.append({ + 'dn': response['dn'], + 'uid': response['attributes'][current_app.config.get('LDAP_MAIL_UID_ATTRIBUTE', 'uid')][0], + 'receivers': response['attributes'].get(current_app.config.get('LDAP_MAIL_RECEIVERS_ATTRIBUTE', 'mailacceptinggeneralid'), []), + 'destinations': response['attributes'].get(current_app.config.get('LDAP_MAIL_DESTINATIONS_ATTRIBUTE', 'maildrop'), []), + }) + return mails + +def upgrade(): + # Load LDAP data first, so we fail as early as possible + ldap_mails = get_ldap_mails() + ldap_users = get_ldap_users() + ldap_groups = get_ldap_groups() + meta = sa.MetaData(bind=op.get_bind()) + + mail_table = op.create_table('mail', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('uid', sa.String(length=32), nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), # tmp + sa.PrimaryKeyConstraint('id', name=op.f('pk_mail')), + sa.UniqueConstraint('uid', name=op.f('uq_mail_uid')) + ) + mail_receive_address_table = op.create_table('mail_receive_address', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('mail_id', sa.Integer(), nullable=True), + sa.Column('mail_dn', sa.String(length=128), nullable=False), # tmp + sa.Column('address', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['mail_id'], ['mail.id'], name=op.f('fk_mail_receive_address_mail_id_mail'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mail_receive_address')) + ) + mail_destination_address_table = op.create_table('mail_destination_address', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('mail_id', sa.Integer(), nullable=True), + sa.Column('mail_dn', sa.String(length=128), nullable=False), # tmp + sa.Column('address', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['mail_id'], ['mail.id'], name=op.f('fk_mail_destination_address_mail_id_mail'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mail_destination_address')) + ) + op.bulk_insert(mail_table, [{'uid': mail['uid'], 'dn': mail['dn']} for mail in ldap_mails]) + rows = [] + for mail in ldap_mails: + rows += [{'mail_dn': mail['dn'], 'address': address} for address in mail['receivers']] + op.bulk_insert(mail_receive_address_table, rows) + op.execute(mail_receive_address_table.update().values(mail_id=sa.select([mail_table.c.id]).where(mail_receive_address_table.c.mail_dn==mail_table.c.dn).limit(1).as_scalar())) + rows = [] + for mail in ldap_mails: + rows += [{'mail_dn': mail['dn'], 'address': address} for address in mail['destinations']] + op.bulk_insert(mail_destination_address_table, rows) + op.execute(mail_destination_address_table.update().values(mail_id=sa.select([mail_table.c.id]).where(mail_destination_address_table.c.mail_dn==mail_table.c.dn).limit(1).as_scalar())) + with op.batch_alter_table('mail', schema=None) as batch_op: + batch_op.drop_column('dn') + with op.batch_alter_table('mail_destination_address', copy_from=mail_destination_address_table) as batch_op: + batch_op.alter_column('mail_id', existing_type=sa.Integer(), nullable=False) + batch_op.drop_column('mail_dn') + with op.batch_alter_table('mail_receive_address', copy_from=mail_receive_address_table) as batch_op: + batch_op.alter_column('mail_id', existing_type=sa.Integer(), nullable=False) + batch_op.drop_column('mail_dn') + + user_table = op.create_table('user', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), # tmp + sa.Column('unix_uid', sa.Integer(), nullable=False), + sa.Column('loginname', sa.String(length=32), nullable=False), + sa.Column('displayname', sa.String(length=128), nullable=False), + sa.Column('mail', sa.String(length=128), nullable=False), + sa.Column('pwhash', sa.String(length=256), nullable=True), + sa.Column('is_service_user', sa.Boolean(name=op.f('ck_user_is_service_user')), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), + sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')), + sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid')) + ) + op.bulk_insert(user_table, ldap_users) + + group_table = op.create_table('group', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), # tmp + sa.Column('unix_gid', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=32), nullable=False), + sa.Column('description', sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_group')), + sa.UniqueConstraint('name', name=op.f('uq_group_name')), + sa.UniqueConstraint('unix_gid', name=op.f('uq_group_unix_gid')) + ) + op.bulk_insert(group_table, [{'dn': group['dn'], 'unix_gid': group['unix_gid'], 'name': group['name'], 'description': group['description']} for group in ldap_groups]) + user_groups_table = op.create_table('user_groups', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), # tmp + sa.Column('user_dn', sa.String(length=128), nullable=False), # tmp + sa.Column('user_id', sa.Integer(), nullable=True), # tmp nullable + sa.Column('group_dn', sa.String(length=128), nullable=False), # tmp + sa.Column('group_id', sa.Integer(), nullable=True), # tmp nullable + sa.ForeignKeyConstraint(['group_id'], ['group.id'], name=op.f('fk_user_groups_group_id_group'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_user_groups_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_user_groups')), + ) + rows = [] + for group in ldap_groups: + rows += [{'group_dn': group['dn'], 'user_dn': member_dn} for member_dn in group['member_dns']] + op.bulk_insert(user_groups_table, rows) + op.execute(user_groups_table.update().values(user_id=sa.select([user_table.c.id]).where(user_groups_table.c.user_dn==user_table.c.dn).as_scalar())) + op.execute(user_groups_table.update().values(group_id=sa.select([group_table.c.id]).where(user_groups_table.c.group_dn==group_table.c.dn).as_scalar())) + # Delete member objects that are not users (like the "dummy" object) + op.execute(user_groups_table.delete().where(sa.or_(user_groups_table.c.user_id==None, user_groups_table.c.group_id==None))) + with op.batch_alter_table('user_groups', copy_from=user_groups_table) as batch_op: + batch_op.alter_column('user_id', existing_type=sa.Integer(), nullable=False) + batch_op.alter_column('group_id', existing_type=sa.Integer(), nullable=False) + batch_op.drop_column('group_dn') + batch_op.alter_column('id', existing_type=sa.Integer(), nullable=True, autoincrement=False) + batch_op.drop_constraint('pk_user_groups', 'primary') + batch_op.create_primary_key('pk_user_groups', ['user_id', 'group_id']) + batch_op.drop_column('id') + batch_op.drop_column('user_dn') + + with op.batch_alter_table('role-group', schema=None) as batch_op: + batch_op.add_column(sa.Column('group_id', sa.Integer(), nullable=True)) + role_groups_table = sa.Table('role-group', meta, + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('group_dn', sa.String(length=128), nullable=False), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('requires_mfa', sa.Boolean(create_constraint=False), nullable=False), + sa.CheckConstraint('requires_mfa in (0,1)', name=op.f('ck_role-group_requires_mfa')), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role-group_role_id_role')), + sa.PrimaryKeyConstraint('role_id', 'group_dn', name=op.f('pk_role-group')) + ) + op.execute(role_groups_table.update().values(group_id=sa.select([group_table.c.id]).where(role_groups_table.c.group_dn==group_table.c.dn).as_scalar())) + op.execute(role_groups_table.delete().where(role_groups_table.c.group_id==None)) + with op.batch_alter_table('role-group', copy_from=role_groups_table) as batch_op: + batch_op.drop_constraint('ck_role-group_requires_mfa', 'check') + batch_op.create_check_constraint('ck_role_groups_requires_mfa', role_groups_table.c.requires_mfa.in_([0,1])) + batch_op.drop_constraint('fk_role-group_role_id_role', 'foreignkey') + batch_op.drop_constraint('pk_role-group', 'primary') + batch_op.create_primary_key('pk_role_groups', ['role_id', 'group_id']) + batch_op.create_foreign_key(batch_op.f('fk_role_groups_role_id_role'), 'role', ['role_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.create_foreign_key(batch_op.f('fk_role_groups_group_id_group'), 'group', ['group_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_column('group_dn') + op.rename_table('role-group', 'role_groups') + + with op.batch_alter_table('role-user', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + role_members_table = sa.Table('role-user', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role-user_role_id_role')), + sa.UniqueConstraint('dn', 'role_id', name='uq_role-user_dn'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_role-user')) + ) + op.execute(role_members_table.update().values(user_id=sa.select([user_table.c.id]).where(role_members_table.c.dn==user_table.c.dn).as_scalar())) + op.execute(role_members_table.delete().where(role_members_table.c.user_id==None)) + with op.batch_alter_table('role-user', copy_from=role_members_table) as batch_op: + batch_op.alter_column('user_id', existing_type=sa.Integer(), nullable=False) + batch_op.alter_column('role_id', existing_type=sa.Integer(), nullable=False) + batch_op.drop_constraint('fk_role-user_role_id_role', 'foreignkey') + batch_op.drop_constraint('uq_role-user_dn', 'unique') + batch_op.create_foreign_key(batch_op.f('fk_role_members_role_id_role'), 'role', ['role_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.create_foreign_key(batch_op.f('fk_role_members_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_column('dn') + batch_op.alter_column('id', existing_type=sa.Integer(), nullable=True, autoincrement=False) + batch_op.drop_constraint('pk_role-user', 'primary') + batch_op.create_primary_key('pk_role_members', ['role_id', 'user_id']) + batch_op.drop_column('id') + op.rename_table('role-user', 'role_members') + + with op.batch_alter_table('device_login_confirmation', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + batch_op.create_unique_constraint(batch_op.f('uq_device_login_confirmation_user_id'), ['user_id']) + device_login_confirmation = sa.Table('device_login_confirmation', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('initiation_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_dn', sa.String(length=128), nullable=False), + sa.Column('code0', sa.String(length=32), nullable=False), + sa.Column('code1', sa.String(length=32), nullable=False), + sa.ForeignKeyConstraint(['initiation_id'], ['device_login_initiation.id'], name=op.f('fk_device_login_confirmation_initiation_id_')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_device_login_confirmation')), + sa.UniqueConstraint('initiation_id', 'code0', name='uq_device_login_confirmation_initiation_id_code0'), + sa.UniqueConstraint('initiation_id', 'code1', name='uq_device_login_confirmation_initiation_id_code1'), + sa.UniqueConstraint('user_id', name=op.f('uq_device_login_confirmation_user_id')) + ) + op.execute(device_login_confirmation.update().values(user_id=sa.select([user_table.c.id]).where(device_login_confirmation.c.user_dn==user_table.c.dn).as_scalar())) + op.execute(device_login_confirmation.delete().where(device_login_confirmation.c.user_id==None)) + with op.batch_alter_table('device_login_confirmation', copy_from=device_login_confirmation) as batch_op: + batch_op.create_foreign_key(batch_op.f('fk_device_login_confirmation_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_constraint('fk_device_login_confirmation_initiation_id_', type_='foreignkey') + batch_op.create_foreign_key('fk_device_login_confirmation_initiation_id_', 'device_login_initiation', ['initiation_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.drop_column('user_dn') + + with op.batch_alter_table('invite', schema=None) as batch_op: + batch_op.add_column(sa.Column('creator_id', sa.Integer(), nullable=True)) + batch_op.create_foreign_key(batch_op.f('fk_invite_creator_id_user'), 'user', ['creator_id'], ['id'], onupdate='CASCADE') + invite = sa.Table('invite', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=True), + sa.Column('creator_dn', sa.String(length=128), nullable=True), + sa.Column('valid_until', sa.DateTime(), nullable=False), + sa.Column('single_use', sa.Boolean(name=op.f('ck_invite_single_use')), nullable=False), + sa.Column('allow_signup', sa.Boolean(name=op.f('ck_invite_allow_signup')), nullable=False), + sa.Column('used', sa.Boolean(name=op.f('ck_invite_used')), nullable=False), + sa.Column('disabled', sa.Boolean(name=op.f('ck_invite_disabled')), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['user.id'], name=op.f('fk_invite_creator_id_user'), onupdate='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_invite')), + sa.UniqueConstraint('token', name=op.f('uq_invite_token')) + ) + op.execute(invite.update().values(creator_id=sa.select([user_table.c.id]).where(invite.c.creator_dn==user_table.c.dn).as_scalar())) + with op.batch_alter_table('invite', copy_from=invite) as batch_op: + batch_op.drop_column('creator_dn') + + with op.batch_alter_table('invite_grant', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + invite_grant = sa.Table('invite_grant', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('invite_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_dn', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['invite_id'], ['invite.id'], name=op.f('fk_invite_grant_invite_id_invite')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_invite_grant')) + ) + op.execute(invite_grant.update().values(user_id=sa.select([user_table.c.id]).where(invite_grant.c.user_dn==user_table.c.dn).as_scalar())) + op.execute(invite_grant.delete().where(invite_grant.c.user_id==None)) + with op.batch_alter_table('invite_grant', copy_from=invite_grant) as batch_op: + batch_op.create_foreign_key(batch_op.f('fk_invite_grant_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_constraint('fk_invite_grant_invite_id_invite', type_='foreignkey') + batch_op.create_foreign_key(batch_op.f('fk_invite_grant_invite_id_invite'), 'invite', ['invite_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.drop_column('user_dn') + + with op.batch_alter_table('mfa_method', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + batch_op.create_foreign_key(batch_op.f('fk_mfa_method_user_id_user'), 'user', ['user_id'], ['id']) + mfa_method = sa.Table('mfa_method', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('type', sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', name='ck_mfa_method_type'), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('name', sa.String(length=128), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('dn', sa.String(length=128), nullable=False), + sa.Column('recovery_salt', sa.String(length=64), nullable=True), + sa.Column('recovery_hash', sa.String(length=256), nullable=True), + sa.Column('totp_key', sa.String(length=64), nullable=True), + sa.Column('webauthn_cred', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_mfa_method_user_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mfa_method')) + ) + op.execute(mfa_method.update().values(user_id=sa.select([user_table.c.id]).where(mfa_method.c.dn==user_table.c.dn).as_scalar())) + op.execute(mfa_method.delete().where(mfa_method.c.user_id==None)) + with op.batch_alter_table('mfa_method', copy_from=mfa_method) as batch_op: + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.alter_column('created', existing_type=sa.DateTime(), nullable=False) + batch_op.alter_column('type', existing_type=sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', name='ck_mfa_method_type'), nullable=False) + batch_op.drop_constraint('fk_mfa_method_user_id_user', type_='foreignkey') + batch_op.create_foreign_key(batch_op.f('fk_mfa_method_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_column('dn') + + with op.batch_alter_table('oauth2grant', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + oauth2grant = sa.Table('oauth2grant', meta, + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_dn', sa.String(length=128), nullable=False), + sa.Column('client_id', sa.String(length=40), nullable=True), + sa.Column('code', sa.String(length=255), nullable=False), + sa.Column('redirect_uri', sa.String(length=255), nullable=True), + sa.Column('expires', sa.DateTime(), nullable=True), + sa.Column('_scopes', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2grant')), + sa.Index('ix_oauth2grant_code', 'code') + ) + op.execute(oauth2grant.update().values(user_id=sa.select([user_table.c.id]).where(oauth2grant.c.user_dn==user_table.c.dn).as_scalar())) + op.execute(oauth2grant.delete().where(oauth2grant.c.user_id==None)) + with op.batch_alter_table('oauth2grant', copy_from=oauth2grant) as batch_op: + batch_op.create_foreign_key(batch_op.f('fk_oauth2grant_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.alter_column('_scopes', nullable=False, existing_type=sa.Text()) + batch_op.alter_column('client_id', nullable=False, existing_type=sa.String(length=40)) + batch_op.alter_column('expires', nullable=False, existing_type=sa.DateTime()) + batch_op.alter_column('redirect_uri', nullable=False, existing_type=sa.String(length=255)) + batch_op.drop_column('user_dn') + + with op.batch_alter_table('oauth2token', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + oauth2token = sa.Table('oauth2token', meta, + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_dn', sa.String(length=128), nullable=False), + sa.Column('client_id', sa.String(length=40), nullable=True), + sa.Column('token_type', sa.String(length=40), nullable=True), + sa.Column('access_token', sa.String(length=255), nullable=True), + sa.Column('refresh_token', sa.String(length=255), nullable=True), + sa.Column('expires', sa.DateTime(), nullable=True), + sa.Column('_scopes', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2token')), + sa.UniqueConstraint('access_token', name=op.f('uq_oauth2token_access_token')), + sa.UniqueConstraint('refresh_token', name=op.f('uq_oauth2token_refresh_token')) + ) + op.execute(oauth2token.update().values(user_id=sa.select([user_table.c.id]).where(oauth2token.c.user_dn==user_table.c.dn).as_scalar())) + op.execute(oauth2token.delete().where(oauth2token.c.user_id==None)) + with op.batch_alter_table('oauth2token', copy_from=oauth2token) as batch_op: + batch_op.create_foreign_key(batch_op.f('fk_oauth2token_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.alter_column('_scopes', nullable=False, existing_type=sa.Text()) + batch_op.alter_column('access_token', nullable=False, existing_type=sa.String(length=255)) + batch_op.alter_column('client_id', nullable=False, existing_type=sa.String(length=40)) + batch_op.alter_column('expires', nullable=False, existing_type=sa.DateTime()) + batch_op.alter_column('refresh_token', nullable=False, existing_type=sa.String(length=255)) + batch_op.alter_column('token_type', nullable=False, existing_type=sa.String(length=40)) + batch_op.drop_column('user_dn') + + with op.batch_alter_table('role', schema=None) as batch_op: + batch_op.add_column(sa.Column('moderator_group_id', sa.Integer(), nullable=True)) + role = sa.Table('role', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=32), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('moderator_group_id', sa.Integer(), nullable=True), + sa.Column('moderator_group_dn', sa.String(length=128), nullable=True), + sa.Column('locked', sa.Boolean(name=op.f('ck_role_locked')), nullable=False), + sa.Column('is_default', sa.Boolean(name=op.f('ck_role_is_default')), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_role')), + sa.UniqueConstraint('name', name=op.f('uq_role_name')) + ) + op.execute(role.update().values(moderator_group_id=sa.select([group_table.c.id]).where(role.c.moderator_group_dn==group_table.c.dn).as_scalar())) + with op.batch_alter_table('role', copy_from=role) as batch_op: + batch_op.create_foreign_key(batch_op.f('fk_role_moderator_group_id_group'), 'group', ['moderator_group_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + batch_op.alter_column('description', existing_type=sa.Text(), nullable=False) + batch_op.alter_column('name', existing_type=sa.String(length=32), nullable=False) + batch_op.drop_column('moderator_group_dn') + + with op.batch_alter_table('signup', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + batch_op.create_unique_constraint(batch_op.f('uq_signup_user_id'), ['user_id']) + signup = sa.Table('signup', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('loginname', sa.Text(), nullable=True), + sa.Column('displayname', sa.Text(), nullable=True), + sa.Column('mail', sa.Text(), nullable=True), + sa.Column('pwhash', sa.Text(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_dn', sa.String(length=128), nullable=True), + sa.Column('type', sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_signup')), + sa.UniqueConstraint('user_id', name=op.f('uq_signup_user_id')) + ) + op.execute(signup.update().values(user_id=sa.select([user_table.c.id]).where(signup.c.user_dn==user_table.c.dn).as_scalar())) + with op.batch_alter_table('signup', copy_from=signup) as batch_op: + batch_op.create_foreign_key(batch_op.f('fk_signup_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_column('user_dn') + + with op.batch_alter_table('mailToken', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + mail_token = sa.Table('mailToken', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('loginname', sa.String(length=32), nullable=True), + sa.Column('newmail', sa.String(length=255), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mailToken')) + ) + op.execute(mail_token.update().values(user_id=sa.select([user_table.c.id]).where(mail_token.c.loginname==user_table.c.loginname).as_scalar())) + op.execute(mail_token.delete().where(mail_token.c.user_id==None)) + with op.batch_alter_table('mailToken', copy_from=mail_token) as batch_op: + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.create_foreign_key(batch_op.f('fk_mailToken_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_column('loginname') + + with op.batch_alter_table('passwordToken', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + password_token = sa.Table('passwordToken', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('loginname', sa.String(length=32), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_passwordToken')) + ) + op.execute(password_token.update().values(user_id=sa.select([user_table.c.id]).where(password_token.c.loginname==user_table.c.loginname).as_scalar())) + op.execute(password_token.delete().where(password_token.c.user_id==None)) + with op.batch_alter_table('passwordToken', copy_from=password_token) as batch_op: + batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer()) + batch_op.alter_column('created', existing_type=sa.DateTime(), nullable=False) + batch_op.create_foreign_key(batch_op.f('fk_passwordToken_user_id_user'), 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.drop_column('loginname') + + with op.batch_alter_table('group', copy_from=group_table) as batch_op: + batch_op.drop_column('dn') + with op.batch_alter_table('user', copy_from=user_table) as batch_op: + batch_op.drop_column('dn') + + # These changes have nothing todo with the LDAP-to-DB migration, but since we add onupdate/ondelete clauses everywhere else ... + invite_roles = sa.Table('invite_roles', meta, + sa.Column('invite_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['invite_id'], ['invite.id'], name=op.f('fk_invite_roles_invite_id_invite')), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_invite_roles_role_id_role')), + sa.PrimaryKeyConstraint('invite_id', 'role_id', name=op.f('pk_invite_roles')) + ) + with op.batch_alter_table('invite_roles', copy_from=invite_roles) as batch_op: + batch_op.drop_constraint('fk_invite_roles_role_id_role', type_='foreignkey') + batch_op.drop_constraint('fk_invite_roles_invite_id_invite', type_='foreignkey') + batch_op.create_foreign_key(batch_op.f('fk_invite_roles_role_id_role'), 'role', ['role_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.create_foreign_key(batch_op.f('fk_invite_roles_invite_id_invite'), 'invite', ['invite_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + invite_signup = sa.Table('invite_signup', meta, + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('invite_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['id'], ['signup.id'], name=op.f('fk_invite_signup_id_signup'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['invite_id'], ['invite.id'], name=op.f('fk_invite_signup_invite_id_invite'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_invite_signup')) + ) + with op.batch_alter_table('invite_signup', copy_from=invite_signup) as batch_op: + batch_op.drop_constraint('fk_invite_signup_id_signup', type_='foreignkey') + batch_op.drop_constraint('fk_invite_signup_invite_id_invite', type_='foreignkey') + batch_op.create_foreign_key(batch_op.f('fk_invite_signup_id_signup'), 'signup', ['id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.create_foreign_key(batch_op.f('fk_invite_signup_invite_id_invite'), 'invite', ['invite_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + role_inclusion = sa.Table('role-inclusion', meta, + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('included_role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['included_role_id'], ['role.id'], name=op.f('fk_role-inclusion_included_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role-inclusion_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'included_role_id', name=op.f('pk_role-inclusion')) + ) + with op.batch_alter_table('role-inclusion', copy_from=role_inclusion) as batch_op: + batch_op.drop_constraint('fk_role-inclusion_role_id_role', type_='foreignkey') + batch_op.drop_constraint('fk_role-inclusion_included_role_id_role', type_='foreignkey') + batch_op.create_foreign_key(batch_op.f('fk_role-inclusion_role_id_role'), 'role', ['role_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + batch_op.create_foreign_key(batch_op.f('fk_role-inclusion_included_role_id_role'), 'role', ['included_role_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + +def downgrade(): + # The downgrade is incomplete as it does not sync changes back to LDAP. The + # code is only here to keep check_migrations.py working. + if not current_app.testing: + raise Exception('Downgrade is not supported') + + # Load LDAP data first, so we fail as early as possible + ldap_users = get_ldap_users() + ldap_groups = get_ldap_groups() + + meta = sa.MetaData(bind=op.get_bind()) + + # These changes have nothing todo with the LDAP to DB migration + role_inclusion = sa.Table('role-inclusion', meta, + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('included_role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['included_role_id'], ['role.id'], name=op.f('fk_role-inclusion_included_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role-inclusion_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'included_role_id', name=op.f('pk_role-inclusion')) + ) + with op.batch_alter_table('role-inclusion', copy_from=role_inclusion) as batch_op: + batch_op.drop_constraint(batch_op.f('fk_role-inclusion_included_role_id_role'), type_='foreignkey') + batch_op.drop_constraint(batch_op.f('fk_role-inclusion_role_id_role'), type_='foreignkey') + batch_op.create_foreign_key('fk_role-inclusion_included_role_id_role', 'role', ['included_role_id'], ['id']) + batch_op.create_foreign_key('fk_role-inclusion_role_id_role', 'role', ['role_id'], ['id']) + invite_signup = sa.Table('invite_signup', meta, + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('invite_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['id'], ['signup.id'], name=op.f('fk_invite_signup_id_signup'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['invite_id'], ['invite.id'], name=op.f('fk_invite_signup_invite_id_invite'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_invite_signup')) + ) + with op.batch_alter_table('invite_signup', copy_from=invite_signup) as batch_op: + batch_op.drop_constraint(batch_op.f('fk_invite_signup_invite_id_invite'), type_='foreignkey') + batch_op.drop_constraint(batch_op.f('fk_invite_signup_id_signup'), type_='foreignkey') + batch_op.create_foreign_key('fk_invite_signup_invite_id_invite', 'invite', ['invite_id'], ['id']) + batch_op.create_foreign_key('fk_invite_signup_id_signup', 'signup', ['id'], ['id']) + invite_roles = sa.Table('invite_roles', meta, + sa.Column('invite_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['invite_id'], ['invite.id'], name=op.f('fk_invite_roles_invite_id_invite'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_invite_roles_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('invite_id', 'role_id', name=op.f('pk_invite_roles')) + ) + with op.batch_alter_table('invite_roles', copy_from=invite_roles) as batch_op: + batch_op.drop_constraint(batch_op.f('fk_invite_roles_invite_id_invite'), type_='foreignkey') + batch_op.drop_constraint(batch_op.f('fk_invite_roles_role_id_role'), type_='foreignkey') + batch_op.create_foreign_key('fk_invite_roles_invite_id_invite', 'invite', ['invite_id'], ['id']) + batch_op.create_foreign_key('fk_invite_roles_role_id_role', 'role', ['role_id'], ['id']) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('dn', sa.String(length=128), nullable=True)) # temporarily nullable + user_table = sa.Table('user', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), + sa.Column('unix_uid', sa.Integer(), nullable=False), + sa.Column('loginname', sa.String(length=32), nullable=False), + sa.Column('displayname', sa.String(length=128), nullable=False), + sa.Column('mail', sa.String(length=128), nullable=False), + sa.Column('pwhash', sa.String(length=256), nullable=True), + sa.Column('is_service_user', sa.Boolean(name=op.f('ck_user_is_service_user')), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), + sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')), + sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid')) + ) + user_dn_map_table = op.create_table('user_dn_map', # deleted later + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), + sa.Column('loginname', sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_user_dn_map')), + sa.UniqueConstraint('dn', name=op.f('uq_user_dn_map_dn')), + sa.UniqueConstraint('loginname', name=op.f('uq_user_dn_map_loginname')) + ) + rows = [{'dn': user['dn'], 'loginname': user['loginname']} for user in ldap_users] + op.bulk_insert(user_dn_map_table, rows) + op.execute(user_table.update().values(dn=sa.select([user_dn_map_table.c.dn]).where(user_table.c.loginname==user_dn_map_table.c.loginname).as_scalar())) + with op.batch_alter_table('user', copy_from=user_table) as batch_op: + pass # Recreate table with dn not nullable + op.drop_table('user_dn_map') + + with op.batch_alter_table('group', schema=None) as batch_op: + batch_op.add_column(sa.Column('dn', sa.String(length=128), nullable=True)) # temporarily nullable + group_table = sa.Table('group', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), + sa.Column('unix_gid', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=32), nullable=False), + sa.Column('description', sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_group')), + sa.UniqueConstraint('name', name=op.f('uq_group_name')), + sa.UniqueConstraint('unix_gid', name=op.f('uq_group_unix_gid')) + ) + group_dn_map_table = op.create_table('group_dn_map', # deleted later + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('dn', sa.String(length=128), nullable=False), + sa.Column('name', sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_group_dn_map')), + sa.UniqueConstraint('dn', name=op.f('uq_group_dn_map_dn')), + sa.UniqueConstraint('name', name=op.f('uq_group_dn_map_name')) + ) + rows = [{'dn': group['dn'], 'name': group['name']} for group in ldap_groups] + op.bulk_insert(group_dn_map_table, rows) + op.execute(group_table.update().values(dn=sa.select([group_dn_map_table.c.dn]).where(group_table.c.name==group_dn_map_table.c.name).as_scalar())) + with op.batch_alter_table('group', copy_from=group_table) as batch_op: + pass # Recreate table with dn not nullable + op.drop_table('group_dn_map') + + with op.batch_alter_table('passwordToken', schema=None) as batch_op: + batch_op.add_column(sa.Column('loginname', sa.String(length=32), nullable=True)) + password_token = sa.Table('passwordToken', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('loginname', sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_passwordToken_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_passwordToken')) + ) + op.execute(password_token.update().values(loginname=sa.select([user_table.c.loginname]).where(password_token.c.user_id==user_table.c.id).as_scalar())) + with op.batch_alter_table('passwordToken', copy_from=password_token) as batch_op: + batch_op.drop_constraint(batch_op.f('fk_passwordToken_user_id_user'), type_='foreignkey') + batch_op.alter_column('created', existing_type=sa.DateTime(), nullable=True) + batch_op.drop_column('user_id') + + with op.batch_alter_table('mailToken', schema=None) as batch_op: + batch_op.add_column(sa.Column('loginname', sa.String(length=32), nullable=True)) + mail_token = sa.Table('mailToken', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('newmail', sa.String(length=255), nullable=True), + sa.Column('loginname', sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_mailToken_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mailToken')) + ) + op.execute(mail_token.update().values(loginname=sa.select([user_table.c.loginname]).where(mail_token.c.user_id==user_table.c.id).as_scalar())) + with op.batch_alter_table('mailToken', copy_from=mail_token) as batch_op: + batch_op.drop_constraint(batch_op.f('fk_mailToken_user_id_user'), type_='foreignkey') + batch_op.drop_column('user_id') + + with op.batch_alter_table('signup', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_dn', sa.String(length=128), nullable=True)) + signup = sa.Table('signup', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('loginname', sa.Text(), nullable=True), + sa.Column('displayname', sa.Text(), nullable=True), + sa.Column('mail', sa.Text(), nullable=True), + sa.Column('pwhash', sa.Text(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_dn', sa.String(length=128), nullable=True), + sa.Column('type', sa.String(length=50), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_signup_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_signup')), + sa.UniqueConstraint('user_id', name=op.f('uq_signup_user_id')) + ) + op.execute(signup.update().values(user_dn=sa.select([user_table.c.dn]).where(signup.c.user_id==user_table.c.id).as_scalar())) + with op.batch_alter_table('signup', copy_from=signup) as batch_op: + batch_op.drop_constraint('fk_signup_user_id_user', 'foreignkey') + batch_op.drop_constraint('uq_signup_user_id', 'unique') + batch_op.drop_column('user_id') + + with op.batch_alter_table('role', schema=None) as batch_op: + batch_op.add_column(sa.Column('moderator_group_dn', sa.String(length=128), nullable=True)) + role = sa.Table('role', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=32), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('moderator_group_id', sa.Integer(), nullable=True), + sa.Column('moderator_group_dn', sa.String(length=128), nullable=True), + sa.Column('locked', sa.Boolean(name=op.f('ck_role_locked')), nullable=False), + sa.Column('is_default', sa.Boolean(name=op.f('ck_role_is_default')), nullable=False), + sa.ForeignKeyConstraint(['moderator_group_id'], ['group.id'], name=op.f('fk_role_moderator_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_role')), + sa.UniqueConstraint('name', name=op.f('uq_role_name')) + ) + op.execute(role.update().values(moderator_group_dn=sa.select([group_table.c.dn]).where(role.c.moderator_group_id==group_table.c.id).as_scalar())) + with op.batch_alter_table('role', copy_from=role) as batch_op: + batch_op.alter_column('description', existing_type=sa.Text(), nullable=True) + batch_op.alter_column('name', existing_type=sa.String(length=32), nullable=True) + batch_op.drop_constraint('fk_role_moderator_group_id_group', 'foreignkey') + batch_op.drop_column('moderator_group_id') + + with op.batch_alter_table('oauth2token', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_dn', sa.String(length=128), nullable=True)) + oauth2token = sa.Table('oauth2token', meta, + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('user_dn', sa.String(length=128), nullable=True), + sa.Column('client_id', sa.String(length=40), nullable=False), + sa.Column('token_type', sa.String(length=40), nullable=False), + sa.Column('access_token', sa.String(length=255), nullable=False), + sa.Column('refresh_token', sa.String(length=255), nullable=False), + sa.Column('expires', sa.DateTime(), nullable=False), + sa.Column('_scopes', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_oauth2token_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2token')), + sa.UniqueConstraint('access_token', name=op.f('uq_oauth2token_access_token')), + sa.UniqueConstraint('refresh_token', name=op.f('uq_oauth2token_refresh_token')) + ) + op.execute(oauth2token.update().values(user_dn=sa.select([user_table.c.dn]).where(oauth2token.c.user_id==user_table.c.id).as_scalar())) + op.execute(oauth2token.delete().where(oauth2token.c.user_dn==None)) + with op.batch_alter_table('oauth2token', copy_from=oauth2token) as batch_op: + batch_op.alter_column('_scopes', nullable=True, existing_type=sa.Text()) + batch_op.alter_column('access_token', nullable=True, existing_type=sa.String(length=255)) + batch_op.alter_column('client_id', nullable=True, existing_type=sa.String(length=40)) + batch_op.alter_column('expires', nullable=True, existing_type=sa.DateTime()) + batch_op.alter_column('refresh_token', nullable=True, existing_type=sa.String(length=255)) + batch_op.alter_column('token_type', nullable=True, existing_type=sa.String(length=40)) + batch_op.drop_constraint('fk_oauth2token_user_id_user', 'foreignkey') + batch_op.drop_column('user_id') + + with op.batch_alter_table('oauth2grant', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_dn', sa.String(length=128), nullable=True)) + oauth2grant = sa.Table('oauth2grant', meta, + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('user_dn', sa.String(length=128), nullable=True), + sa.Column('client_id', sa.String(length=40), nullable=False), + sa.Column('code', sa.String(length=255), nullable=False), + sa.Column('redirect_uri', sa.String(length=255), nullable=False), + sa.Column('expires', sa.DateTime(), nullable=False), + sa.Column('_scopes', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_oauth2grant_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2grant')), + sa.Index('ix_oauth2grant_code', 'code') + ) + op.execute(oauth2grant.update().values(user_dn=sa.select([user_table.c.dn]).where(oauth2grant.c.user_id==user_table.c.id).as_scalar())) + op.execute(oauth2grant.delete().where(oauth2grant.c.user_dn==None)) + with op.batch_alter_table('oauth2grant', copy_from=oauth2grant) as batch_op: + batch_op.alter_column('_scopes', nullable=True, existing_type=sa.Text()) + batch_op.alter_column('client_id', nullable=True, existing_type=sa.String(length=40)) + batch_op.alter_column('expires', nullable=True, existing_type=sa.DateTime()) + batch_op.alter_column('redirect_uri', nullable=True, existing_type=sa.String(length=255)) + batch_op.drop_constraint('fk_oauth2grant_user_id_user', 'foreignkey') + batch_op.drop_column('user_id') + + with op.batch_alter_table('mfa_method', schema=None) as batch_op: + batch_op.add_column(sa.Column('dn', sa.String(length=128), nullable=True)) + mfa_method = sa.Table('mfa_method', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('type', sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', name='ck_mfa_method_type'), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=128), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('dn', sa.String(length=128), nullable=True), + sa.Column('recovery_salt', sa.String(length=64), nullable=True), + sa.Column('recovery_hash', sa.String(length=256), nullable=True), + sa.Column('totp_key', sa.String(length=64), nullable=True), + sa.Column('webauthn_cred', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_mfa_method_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mfa_method')) + ) + op.execute(mfa_method.update().values(dn=sa.select([user_table.c.dn]).where(mfa_method.c.user_id==user_table.c.id).as_scalar())) + op.execute(mfa_method.delete().where(mfa_method.c.dn==None)) + with op.batch_alter_table('mfa_method', copy_from=mfa_method) as batch_op: + batch_op.drop_constraint('fk_mfa_method_user_id_user', 'foreignkey') + batch_op.alter_column('type', existing_type=sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', name='ck_mfa_method_type'), nullable=True) + batch_op.alter_column('created', existing_type=sa.DateTime(), nullable=True) + batch_op.drop_column('user_id') + + with op.batch_alter_table('invite_grant', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_dn', sa.String(length=128), nullable=True)) + invite_grant = sa.Table('invite_grant', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('invite_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('user_dn', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['invite_id'], ['invite.id'], name=op.f('fk_invite_grant_invite_id_invite'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_invite_grant_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_invite_grant')) + ) + op.execute(invite_grant.update().values(user_dn=sa.select([user_table.c.dn]).where(invite_grant.c.user_id==user_table.c.id).as_scalar())) + op.execute(invite_grant.delete().where(invite_grant.c.user_dn==None)) + with op.batch_alter_table('invite_grant', copy_from=invite_grant) as batch_op: + batch_op.drop_constraint('fk_invite_grant_user_id_user', 'foreignkey') + batch_op.drop_constraint(batch_op.f('fk_invite_grant_invite_id_invite'), type_='foreignkey') + batch_op.create_foreign_key('fk_invite_grant_invite_id_invite', 'invite', ['invite_id'], ['id']) + batch_op.drop_column('user_id') + + with op.batch_alter_table('invite', schema=None) as batch_op: + batch_op.add_column(sa.Column('creator_dn', sa.String(length=128), nullable=True)) + invite = sa.Table('invite', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=128), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=True), + sa.Column('creator_dn', sa.String(length=128), nullable=True), + sa.Column('valid_until', sa.DateTime(), nullable=False), + sa.Column('single_use', sa.Boolean(name=op.f('ck_invite_single_use')), nullable=False), + sa.Column('allow_signup', sa.Boolean(name=op.f('ck_invite_allow_signup')), nullable=False), + sa.Column('used', sa.Boolean(name=op.f('ck_invite_used')), nullable=False), + sa.Column('disabled', sa.Boolean(name=op.f('ck_invite_disabled')), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['user.id'], name=op.f('fk_invite_creator_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_invite')), + sa.UniqueConstraint('token', name=op.f('uq_invite_token')) + ) + op.execute(invite.update().values(creator_dn=sa.select([user_table.c.dn]).where(invite.c.creator_id==user_table.c.id).as_scalar())) + with op.batch_alter_table('invite', copy_from=invite) as batch_op: + batch_op.drop_constraint('fk_invite_creator_id_user', 'foreignkey') + batch_op.drop_column('creator_id') + + with op.batch_alter_table('device_login_confirmation', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_dn', sa.String(length=128), nullable=True)) + device_login_confirmation = sa.Table('device_login_confirmation', meta, + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('initiation_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('user_dn', sa.String(length=128), nullable=False), + sa.Column('code0', sa.String(length=32), nullable=False), + sa.Column('code1', sa.String(length=32), nullable=False), + sa.ForeignKeyConstraint(['initiation_id'], ['device_login_initiation.id'], name=op.f('fk_device_login_confirmation_initiation_id_')), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_device_login_confirmation_user_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_device_login_confirmation')), + sa.UniqueConstraint('initiation_id', 'code0', name='uq_device_login_confirmation_initiation_id_code0'), + sa.UniqueConstraint('initiation_id', 'code1', name='uq_device_login_confirmation_initiation_id_code1'), + sa.UniqueConstraint('user_dn', name=op.f('uq_device_login_confirmation_user_dn')) + ) + op.execute(device_login_confirmation.update().values(user_dn=sa.select([user_table.c.dn]).where(device_login_confirmation.c.user_id==user_table.c.id).as_scalar())) + op.execute(device_login_confirmation.delete().where(device_login_confirmation.c.user_dn==None)) + with op.batch_alter_table('device_login_confirmation', copy_from=device_login_confirmation) as batch_op: + batch_op.drop_constraint('fk_device_login_confirmation_user_id_user', 'foreignkey') + batch_op.drop_constraint('fk_device_login_confirmation_initiation_id_', type_='foreignkey') + batch_op.create_foreign_key('fk_device_login_confirmation_initiation_id_', 'device_login_initiation', ['initiation_id'], ['id']) + batch_op.drop_column('user_id') + + with op.batch_alter_table('role_members', schema=None) as batch_op: + batch_op.add_column(sa.Column('dn', sa.String(length=128), nullable=True)) + op.rename_table('role_members', 'role-user') + role_members_table = sa.Table('role-user', meta, + sa.Column('dn', sa.String(length=128), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_role_members_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role_members_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'user_id', name=op.f('pk_role_members')) + ) + op.execute(role_members_table.update().values(dn=sa.select([user_table.c.dn]).where(role_members_table.c.user_id==user_table.c.id).as_scalar())) + op.execute(role_members_table.delete().where(role_members_table.c.dn==None)) + with op.batch_alter_table('role-user', copy_from=role_members_table, recreate='always') as batch_op: + batch_op.drop_constraint('fk_role_members_role_id_role', 'foreignkey') + batch_op.create_foreign_key(batch_op.f('fk_role-user_role_id_role'), 'role', ['role_id'], ['id']) + batch_op.alter_column('dn', nullable=False, existing_type=sa.String(length=128)) + batch_op.add_column(sa.Column('id', sa.Integer(), nullable=True)) + batch_op.drop_constraint('fk_role_members_user_id_user', 'foreignkey') + batch_op.drop_constraint('pk_role_members', 'primary') + batch_op.create_primary_key('pk_role-user', ['id']) + batch_op.alter_column('id', autoincrement=True, nullable=False, existing_type=sa.Integer()) + batch_op.create_unique_constraint(batch_op.f('uq_role-user_dn'), ['dn', 'role_id']) + batch_op.alter_column('role_id', existing_type=sa.Integer(), nullable=False) + batch_op.drop_column('user_id') + + with op.batch_alter_table('role_groups', schema=None) as batch_op: + batch_op.add_column(sa.Column('group_dn', sa.String(length=128), nullable=True)) + op.rename_table('role_groups', 'role-group') + role_groups_table = sa.Table('role-group', meta, + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('group_dn', sa.String(length=128), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=False), + sa.Column('requires_mfa', sa.Boolean(create_constraint=False), nullable=False), + sa.CheckConstraint('requires_mfa in (0,1)', name=op.f('ck_role_groups_requires_mfa')), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], name=op.f('fk_role_groups_group_id_group'), onupdate='CASCADE', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role_groups_role_id_role'), onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'group_id', name=op.f('pk_role_groups')) + ) + op.execute(role_groups_table.update().values(group_dn=sa.select([group_table.c.dn]).where(role_groups_table.c.group_id==group_table.c.id).as_scalar())) + op.execute(role_groups_table.delete().where(role_groups_table.c.group_dn==None)) + with op.batch_alter_table('role-group', copy_from=role_groups_table) as batch_op: + batch_op.drop_constraint('fk_role_groups_group_id_group', 'foreignkey') + batch_op.drop_constraint('fk_role_groups_role_id_role', 'foreignkey') + batch_op.drop_constraint('ck_role_groups_requires_mfa', 'check') + batch_op.create_check_constraint('ck_role-group_requires_mfa', role_groups_table.c.requires_mfa.in_([0,1])) + batch_op.alter_column('group_dn', nullable=False, existing_type=sa.String(length=128)) + batch_op.drop_constraint('pk_role_groups', 'primary') + batch_op.create_primary_key('pk_role-group', ['role_id', 'group_dn']) + batch_op.create_foreign_key(batch_op.f('fk_role-group_role_id_role'), 'role', ['role_id'], ['id']) + batch_op.drop_column('group_id') + + op.drop_table('mail_receive_address') + op.drop_table('mail_destination_address') + op.drop_table('mail') + op.drop_table('user_groups') + op.drop_table('user') + op.drop_table('group') diff --git a/uffd/oauth2/models.py b/uffd/oauth2/models.py index 5e27c5d7..211f2c22 100644 --- a/uffd/oauth2/models.py +++ b/uffd/oauth2/models.py @@ -1,10 +1,9 @@ from flask import current_app from flask_babel import get_locale, gettext as _ -from sqlalchemy import Column, Integer, String, DateTime, Text +from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey +from sqlalchemy.orm import relationship -from uffd.ldapalchemy.dbutils import DBRelationship from uffd.database import db -from uffd.user.models import User from uffd.session.models import DeviceLoginInitiation, DeviceLoginType class OAuth2Client: @@ -42,12 +41,12 @@ class OAuth2Client: class OAuth2Grant(db.Model): __tablename__ = 'oauth2grant' - id = Column(Integer, primary_key=True) + id = Column(Integer, primary_key=True, autoincrement=True) - user_dn = Column(String(128)) - user = DBRelationship('user_dn', User, backref='oauth2_grants') + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + user = relationship('User') - client_id = Column(String(40)) + client_id = Column(String(40), nullable=False) @property def client(self): @@ -58,10 +57,10 @@ class OAuth2Grant(db.Model): self.client_id = newclient.client_id code = Column(String(255), index=True, nullable=False) - redirect_uri = Column(String(255)) - expires = Column(DateTime) + redirect_uri = Column(String(255), nullable=False) + expires = Column(DateTime, nullable=False) - _scopes = Column(Text) + _scopes = Column(Text, nullable=False, default='') @property def scopes(self): if self._scopes: @@ -75,12 +74,12 @@ class OAuth2Grant(db.Model): class OAuth2Token(db.Model): __tablename__ = 'oauth2token' - id = Column(Integer, primary_key=True) + id = Column(Integer, primary_key=True, autoincrement=True) - user_dn = Column(String(128)) - user = DBRelationship('user_dn', User, backref='oauth2_tokens') + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + user = relationship('User') - client_id = Column(String(40)) + client_id = Column(String(40), nullable=False) @property def client(self): @@ -91,12 +90,12 @@ class OAuth2Token(db.Model): self.client_id = newclient.client_id # currently only bearer is supported - token_type = Column(String(40)) - access_token = Column(String(255), unique=True) - refresh_token = Column(String(255), unique=True) - expires = Column(DateTime) + token_type = Column(String(40), nullable=False) + access_token = Column(String(255), unique=True, nullable=False) + refresh_token = Column(String(255), unique=True, nullable=False) + expires = Column(DateTime, nullable=False) - _scopes = Column(Text) + _scopes = Column(Text, nullable=False, default='') @property def scopes(self): if self._scopes: diff --git a/uffd/oauth2/views.py b/uffd/oauth2/views.py index 57a7e9bf..4477e0ed 100644 --- a/uffd/oauth2/views.py +++ b/uffd/oauth2/views.py @@ -67,7 +67,7 @@ class UffdRequestValidator(oauthlib.oauth2.RequestValidator): def save_authorization_code(self, client_id, code, oauthreq, *args, **kwargs): expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=100) - grant = OAuth2Grant(user_dn=oauthreq.user.dn, client_id=client_id, code=code['code'], + grant = OAuth2Grant(user=oauthreq.user, client_id=client_id, code=code['code'], redirect_uri=oauthreq.redirect_uri, expires=expires, _scopes=' '.join(oauthreq.scopes)) db.session.add(grant) db.session.commit() @@ -97,11 +97,11 @@ class UffdRequestValidator(oauthlib.oauth2.RequestValidator): db.session.commit() def save_bearer_token(self, token_data, oauthreq, *args, **kwargs): - OAuth2Token.query.filter_by(client_id=oauthreq.client.client_id, user_dn=oauthreq.user.dn).delete() + OAuth2Token.query.filter_by(client_id=oauthreq.client.client_id, user=oauthreq.user).delete() expires_in = token_data.get('expires_in') expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires_in) tok = OAuth2Token( - user_dn=oauthreq.user.dn, + user=oauthreq.user, client_id=oauthreq.client.client_id, token_type=token_data['token_type'], access_token=token_data['access_token'], @@ -234,15 +234,11 @@ def oauth_required(*scopes): @oauth_required('profile') def userinfo(): user = request.oauth.user - # We once exposed the entryUUID here as "ldap_uuid" until realising that it - # can (and does!) change randomly and is therefore entirely useless as an - # indentifier. return jsonify( - id=user.uid, + id=user.unix_uid, name=user.displayname, nickname=user.loginname, email=user.mail, - ldap_dn=user.dn, groups=[group.name for group in user.groups] ) diff --git a/uffd/role/models.py b/uffd/role/models.py index e5b40190..fa60c9be 100644 --- a/uffd/role/models.py +++ b/uffd/role/models.py @@ -1,38 +1,28 @@ from sqlalchemy import Column, String, Integer, Text, ForeignKey, Boolean from sqlalchemy.orm import relationship from sqlalchemy.orm.collections import MappedCollection, collection -from sqlalchemy.ext.declarative import declared_attr -from uffd.ldapalchemy.dbutils import DBRelationship from uffd.database import db -from uffd.user.models import User, Group +from uffd.user.models import User class RoleGroup(db.Model): - __tablename__ = 'role-group' - role_id = Column(Integer(), ForeignKey('role.id'), primary_key=True) - group_dn = Column(String(128), primary_key=True) - requires_mfa = Column(Boolean(), default=False, nullable=False) - + __tablename__ = 'role_groups' + role_id = Column(Integer(), ForeignKey('role.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) role = relationship('Role') - group = DBRelationship('group_dn', Group) - -class RoleUser(db.Model): - __tablename__ = 'role-user' - __table_args__ = ( - db.UniqueConstraint('dn', 'role_id'), - ) - - id = Column(Integer(), primary_key=True, autoincrement=True) - dn = Column(String(128)) + group_id = Column(Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) + group = relationship('Group') + requires_mfa = Column(Boolean(), default=False, nullable=False) - @declared_attr - def role_id(self): - return Column(ForeignKey('role.id')) +# pylint: disable=E1101 +role_members = db.Table('role_members', + db.Column('role_id', db.Integer(), db.ForeignKey('role.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), + db.Column('user_id', db.Integer(), db.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) +) # pylint: disable=E1101 role_inclusion = db.Table('role-inclusion', - Column('role_id', Integer, ForeignKey('role.id'), primary_key=True), - Column('included_role_id', Integer, ForeignKey('role.id'), primary_key=True) + Column('role_id', Integer, ForeignKey('role.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), + Column('included_role_id', Integer, ForeignKey('role.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) ) def flatten_recursive(objs, attr): @@ -72,8 +62,9 @@ def update_user_groups(user): groups_added = groups - current_groups groups_removed = current_groups - groups for group in groups_removed: - user.groups.discard(group) - user.groups.update(groups_added) + user.groups.remove(group) + for group in groups_added: + user.groups.append(group) return groups_added, groups_removed User.update_groups = update_user_groups @@ -90,19 +81,18 @@ class RoleGroupMap(MappedCollection): class Role(db.Model): __tablename__ = 'role' id = Column(Integer(), primary_key=True, autoincrement=True) - name = Column(String(32), unique=True) - description = Column(Text(), default='') + name = Column(String(32), unique=True, nullable=False) + description = Column(Text(), default='', nullable=False) included_roles = relationship('Role', secondary=role_inclusion, primaryjoin=id == role_inclusion.c.role_id, secondaryjoin=id == role_inclusion.c.included_role_id, backref='including_roles') including_roles = [] # overwritten by backref - moderator_group_dn = Column(String(128), nullable=True) - moderator_group = DBRelationship('moderator_group_dn', Group) + moderator_group_id = Column(Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='SET NULL'), nullable=True) + moderator_group = relationship('Group') - db_members = relationship("RoleUser", backref="role", cascade="all, delete-orphan") - members = DBRelationship('db_members', User, RoleUser, backattr='role', backref='roles') + members = relationship('User', secondary='role_members', back_populates='roles') groups = relationship('RoleGroup', collection_class=RoleGroupMap, cascade='all, delete-orphan') diff --git a/uffd/role/templates/role/show.html b/uffd/role/templates/role/show.html index 971d7878..3ee88d69 100644 --- a/uffd/role/templates/role/show.html +++ b/uffd/role/templates/role/show.html @@ -55,7 +55,7 @@ <select class="form-control" id="moderator-group" name="moderator-group" {{ 'disabled' if role.locked }}> <option value="" class="text-muted">{{_("No Moderator Group")}}</option> {% for group in groups %} - <option value="{{ group.dn }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option> + <option value="{{ group.id }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option> {% endfor %} </select> </div> @@ -63,7 +63,7 @@ <span>{{_("Moderators")}}:</span> <ul class="row"> {% for moderator in role.moderator_group.members %} - <li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", uid=moderator.uid) }}">{{ moderator.loginname }}</a></li> + <li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=moderator.id) }}">{{ moderator.loginname }}</a></li> {% endfor %} </ul> </div> @@ -71,7 +71,7 @@ <span>{{_("Members")}}:</span> <ul class="row"> {% for member in role.members|sort(attribute='loginname') %} - <li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", uid=member.uid) }}">{{ member.loginname }}</a></li> + <li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=member.id) }}">{{ member.loginname }}</a></li> {% endfor %} </ul> </div> @@ -108,7 +108,7 @@ </td> <td> {% for group in r.groups_effective|sort(attribute='name') %} - <a href="{{ url_for("group.show", gid=group.gid) }}">{{ group.name }}</a>{{ ', ' if not loop.last }} + <a href="{{ url_for("group.show", id=group.id) }}">{{ group.name }}</a>{{ ', ' if not loop.last }} {% endfor %} </td> </tr> @@ -131,14 +131,14 @@ </thead> <tbody> {% for group in groups|sort(attribute="name") %} - <tr id="group-{{ group.gid }}"> + <tr id="group-{{ group.id }}"> <td> <div class="form-check"> - <input class="form-check-input" type="checkbox" id="group-{{ group.gid }}-checkbox" name="group-{{ group.gid }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %} {{ 'disabled' if role.locked }}> + <input class="form-check-input" type="checkbox" id="group-{{ group.id }}-checkbox" name="group-{{ group.id }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %} {{ 'disabled' if role.locked }}> </div> </td> <td> - <a href="{{ url_for("group.show", gid=group.gid) }}"> + <a href="{{ url_for("group.show", id=group.id) }}"> {{ group.name }} </a> </td> @@ -147,7 +147,7 @@ </td> <td> <div class="form-check"> - <input class="form-check-input" type="checkbox" id="group-mfa-{{ group.gid }}-checkbox" name="group-mfa-{{ group.gid }}" value="1" aria-label="enabled" {% if group in role.groups and role.groups[group].requires_mfa %}checked{% endif %} {{ 'disabled' if role.locked }}> + <input class="form-check-input" type="checkbox" id="group-mfa-{{ group.id }}-checkbox" name="group-mfa-{{ group.id }}" value="1" aria-label="enabled" {% if group in role.groups and role.groups[group].requires_mfa %}checked{% endif %} {{ 'disabled' if role.locked }}> </div> </td> </tr> diff --git a/uffd/role/views.py b/uffd/role/views.py index 7261c63f..94b15e17 100644 --- a/uffd/role/views.py +++ b/uffd/role/views.py @@ -10,7 +10,6 @@ from uffd.role.models import Role, RoleGroup from uffd.user.models import User, Group from uffd.session import login_required from uffd.database import db -from uffd.ldap import ldap bp = Blueprint("role", __name__, template_folder='templates', url_prefix='/role/') @@ -25,12 +24,12 @@ def add_cli_commands(state): groups_added, groups_removed = user.update_groups() if groups_added: consistent = False - print('Adding groups [%s] to user %s'%(', '.join([group.name for group in groups_added]), user.dn)) + print('Adding groups [%s] to user %s'%(', '.join([group.name for group in groups_added]), user.loginname)) if groups_removed: consistent = False - print('Removing groups [%s] from user %s'%(', '.join([group.name for group in groups_removed]), user.dn)) + print('Removing groups [%s] from user %s'%(', '.join([group.name for group in groups_removed]), user.loginname)) if not check_only: - ldap.session.commit() + db.session.commit() if check_only and not consistent: print('No changes were made because --check-only is set') print() @@ -56,9 +55,7 @@ def new(): @bp.route("/<int:roleid>") def show(roleid=None): - # prefetch all users so the ldap orm can cache them and doesn't run one ldap query per user - User.query.all() - role = Role.query.filter_by(id=roleid).one() + role = Role.query.get(roleid) return render_template('role/show.html', role=role, groups=Group.query.all(), roles=Role.query.all()) @bp.route("/<int:roleid>/update", methods=['POST']) @@ -69,12 +66,12 @@ def update(roleid=None): role = Role() db.session.add(role) else: - role = Role.query.filter_by(id=roleid).one() + role = Role.query.get(roleid) role.description = request.values['description'] if not role.locked: role.name = request.values['name'] if not request.values['moderator-group']: - role.moderator_group_dn = None + role.moderator_group = None else: role.moderator_group = Group.query.get(request.values['moderator-group']) for included_role in Role.query.all(): @@ -84,17 +81,16 @@ def update(roleid=None): role.included_roles.remove(included_role) role.groups.clear() for group in Group.query.all(): - if request.values.get(f'group-{group.gid}', False): - role.groups[group] = RoleGroup(requires_mfa=bool(request.values.get(f'group-mfa-{group.gid}', ''))) + if request.values.get(f'group-{group.id}', False): + role.groups[group] = RoleGroup(requires_mfa=bool(request.values.get(f'group-mfa-{group.id}', ''))) role.update_member_groups() db.session.commit() - ldap.session.commit() return redirect(url_for('role.show', roleid=role.id)) @bp.route("/<int:roleid>/del") @csrf_protect(blueprint=bp) def delete(roleid): - role = Role.query.filter_by(id=roleid).one() + role = Role.query.get(roleid) if role.locked: flash(_('Locked roles cannot be deleted')) return redirect(url_for('role.show', roleid=role.id)) @@ -104,13 +100,12 @@ def delete(roleid): for user in old_members: user.update_groups() db.session.commit() - ldap.session.commit() return redirect(url_for('role.index')) @bp.route("/<int:roleid>/unlock") @csrf_protect(blueprint=bp) def unlock(roleid): - role = Role.query.filter_by(id=roleid).one() + role = Role.query.get(roleid) role.locked = False db.session.commit() return redirect(url_for('role.show', roleid=role.id)) @@ -118,22 +113,21 @@ def unlock(roleid): @bp.route("/<int:roleid>/setdefault") @csrf_protect(blueprint=bp) def set_default(roleid): - role = Role.query.filter_by(id=roleid).one() + role = Role.query.get(roleid) if role.is_default: return redirect(url_for('role.show', roleid=role.id)) role.is_default = True for user in set(role.members): if not user.is_service_user: - role.members.discard(user) + role.members.remove(user) role.update_member_groups() db.session.commit() - ldap.session.commit() return redirect(url_for('role.show', roleid=role.id)) @bp.route("/<int:roleid>/unsetdefault") @csrf_protect(blueprint=bp) def unset_default(roleid): - role = Role.query.filter_by(id=roleid).one() + role = Role.query.get(roleid) if not role.is_default: return redirect(url_for('role.show', roleid=role.id)) old_members = set(role.members_effective) @@ -142,5 +136,4 @@ def unset_default(roleid): if not user.is_service_user: user.update_groups() db.session.commit() - ldap.session.commit() return redirect(url_for('role.show', roleid=role.id)) diff --git a/uffd/rolemod/templates/rolemod/show.html b/uffd/rolemod/templates/rolemod/show.html index fbb28c06..c1749468 100644 --- a/uffd/rolemod/templates/rolemod/show.html +++ b/uffd/rolemod/templates/rolemod/show.html @@ -52,7 +52,7 @@ <tr> <td>{{ member.displayname }} ({{ member.loginname }})</td> <td class="text-right"> - <a class="btn btn-danger py-0" href="{{ url_for('rolemod.delete_member', role_id=role.id, member_dn=member.dn) }}">{{_('Remove')}}</a> + <a class="btn btn-danger py-0" href="{{ url_for('rolemod.delete_member', role_id=role.id, member_id=member.id) }}">{{_('Remove')}}</a> </td> </tr> {% endfor %} diff --git a/uffd/rolemod/views.py b/uffd/rolemod/views.py index 58849101..0a99ac3d 100644 --- a/uffd/rolemod/views.py +++ b/uffd/rolemod/views.py @@ -4,15 +4,14 @@ from flask_babel import gettext as _, lazy_gettext from uffd.navbar import register_navbar from uffd.csrf import csrf_protect from uffd.role.models import Role -from uffd.user.models import User +from uffd.user.models import User, Group from uffd.session import login_required from uffd.database import db -from uffd.ldap import ldap bp = Blueprint('rolemod', __name__, template_folder='templates', url_prefix='/rolemod/') def user_is_rolemod(): - return request.user and Role.query.filter(Role.moderator_group_dn.in_(request.user.group_dns)).count() + return request.user and Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).count() @bp.before_request @login_required() @@ -23,13 +22,11 @@ def acl_check(): @bp.route("/") @register_navbar(12, lazy_gettext('Moderation'), icon='user-lock', blueprint=bp, visible=user_is_rolemod) def index(): - roles = Role.query.filter(Role.moderator_group_dn.in_(request.user.group_dns)).all() + roles = Role.query.join(Role.moderator_group).join(Group.members).filter(User.id==request.user.id).all() return render_template('rolemod/list.html', roles=roles) @bp.route("/<int:role_id>") def show(role_id): - # prefetch all users so the ldap orm can cache them and doesn't run one ldap query per user - User.query.all() role = Role.query.get_or_404(role_id) if role.moderator_group not in request.user.groups: abort(403) @@ -49,16 +46,16 @@ def update(role_id): db.session.commit() return redirect(url_for('.show', role_id=role.id)) -@bp.route("/<int:role_id>/delete_member/<member_dn>") +@bp.route("/<int:role_id>/delete_member/<int:member_id>") @csrf_protect(blueprint=bp) -def delete_member(role_id, member_dn): +def delete_member(role_id, member_id): role = Role.query.get_or_404(role_id) if role.moderator_group not in request.user.groups: abort(403) - member = User.query.get_or_404(member_dn) - role.members.discard(member) + member = User.query.get_or_404(member_id) + if member in role.members: + role.members.remove(member) member.update_groups() - ldap.session.commit() db.session.commit() flash(_('Member removed')) return redirect(url_for('.show', role_id=role.id)) diff --git a/uffd/selfservice/models.py b/uffd/selfservice/models.py index 096c8743..b0103f70 100644 --- a/uffd/selfservice/models.py +++ b/uffd/selfservice/models.py @@ -1,6 +1,7 @@ import datetime -from sqlalchemy import Column, String, DateTime, Integer +from sqlalchemy import Column, String, DateTime, Integer, ForeignKey +from sqlalchemy.orm import relationship from uffd.database import db from uffd.utils import token_urlfriendly @@ -9,13 +10,15 @@ class PasswordToken(db.Model): __tablename__ = 'passwordToken' id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), default=token_urlfriendly, nullable=False) - created = Column(DateTime, default=datetime.datetime.now) - loginname = Column(String(32)) + created = Column(DateTime, default=datetime.datetime.now, nullable=False) + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + user = relationship('User') class MailToken(db.Model): __tablename__ = 'mailToken' id = Column(Integer(), primary_key=True, autoincrement=True) token = Column(String(128), default=token_urlfriendly, nullable=False) created = Column(DateTime, default=datetime.datetime.now) - loginname = Column(String(32)) + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) + user = relationship('User') newmail = Column(String(255)) diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py index a5ac6c91..3137aabb 100644 --- a/uffd/selfservice/views.py +++ b/uffd/selfservice/views.py @@ -1,7 +1,7 @@ import datetime import secrets -from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session, abort +from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, abort from flask_babel import gettext as _, lazy_gettext from uffd.navbar import register_navbar @@ -12,7 +12,6 @@ from uffd.selfservice.models import PasswordToken, MailToken from uffd.sendmail import sendmail from uffd.role.models import Role from uffd.database import db -from uffd.ldap import ldap from uffd.ratelimit import host_ratelimit, Ratelimit, format_delay bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix='/self/') @@ -32,36 +31,29 @@ def index(): @csrf_protect(blueprint=bp) @login_required(selfservice_acl_check) def update_profile(): - user = request.user - if request.values['displayname'] != user.displayname: - if user.set_displayname(request.values['displayname']): + if request.values['displayname'] != request.user.displayname: + if request.user.set_displayname(request.values['displayname']): flash(_('Display name changed.')) else: flash(_('Display name is not valid.')) - if request.values['mail'] != user.mail: - send_mail_verification(user.loginname, request.values['mail']) + if request.values['mail'] != request.user.mail: + send_mail_verification(request.user, request.values['mail']) flash(_('We sent you an email, please verify your mail address.')) - ldap.session.commit() + db.session.commit() return redirect(url_for('selfservice.index')) @bp.route("/changepassword", methods=(['POST'])) @csrf_protect(blueprint=bp) @login_required(selfservice_acl_check) def change_password(): - password_changed = False - user = request.user if not request.values['password1'] == request.values['password2']: flash(_('Passwords do not match')) else: - if user.set_password(request.values['password1']): + if request.user.set_password(request.values['password1']): flash(_('Password changed')) - password_changed = True else: flash(_('Invalid password')) - ldap.session.commit() - # When using a user_connection, update the connection on password-change - if password_changed and current_app.config['LDAP_SERVICE_USER_BIND']: - session['user_pw'] = request.values['password1'] + db.session.commit() return redirect(url_for('selfservice.index')) @bp.route("/passwordreset", methods=(['GET', 'POST'])) @@ -118,15 +110,13 @@ def token_password(token_id, token): if not request.values['password1'] == request.values['password2']: flash(_('Passwords do not match, please try again.')) return render_template('selfservice/set_password.html', token=dbtoken) - user = User.query.filter_by(loginname=dbtoken.loginname).one() - if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']): + if not dbtoken.user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']): abort(403) - if not user.set_password(request.values['password1']): + if not dbtoken.user.set_password(request.values['password1']): flash(_('Password ist not valid, please try again.')) return render_template('selfservice/set_password.html', token=dbtoken) db.session.delete(dbtoken) flash(_('New password set')) - ldap.session.commit() db.session.commit() return redirect(url_for('session.login')) @@ -154,13 +144,11 @@ def token_mail(token_id, token): db.session.delete(dbtoken) db.session.commit() return redirect(url_for('selfservice.index')) - user = User.query.filter_by(loginname=dbtoken.loginname).one() - if user != request.user: + if dbtoken.user != request.user: abort(403, description=_('This link was generated for another user. Login as the correct user to continue.')) - user.set_mail(dbtoken.newmail) + dbtoken.user.set_mail(dbtoken.newmail) flash(_('New mail set')) db.session.delete(dbtoken) - ldap.session.commit() db.session.commit() return redirect(url_for('selfservice.index')) @@ -172,36 +160,26 @@ def leave_role(roleid): flash(_('Leaving roles is disabled')) return redirect(url_for('selfservice.index')) role = Role.query.get_or_404(roleid) - role.members.discard(request.user) + role.members.remove(request.user) request.user.update_groups() - ldap.session.commit() db.session.commit() flash(_('You left role %(role_name)s', role_name=role.name)) return redirect(url_for('selfservice.index')) -def send_mail_verification(loginname, newmail): - expired_tokens = MailToken.query.filter(MailToken.created < (datetime.datetime.now() - datetime.timedelta(days=2))).all() - duplicate_tokens = MailToken.query.filter(MailToken.loginname == loginname).all() - for i in expired_tokens + duplicate_tokens: - db.session.delete(i) - token = MailToken() - token.loginname = loginname - token.newmail = newmail +def send_mail_verification(user, newmail): + MailToken.query.filter(db.or_(MailToken.created < (datetime.datetime.now() - datetime.timedelta(days=2)), + MailToken.user == user)).delete() + token = MailToken(user=user, newmail=newmail) db.session.add(token) db.session.commit() - user = User.query.filter_by(loginname=loginname).one() - if not sendmail(newmail, 'Mail verification', 'selfservice/mailverification.mail.txt', user=user, token=token): flash(_('Mail to "%(mail_address)s" could not be sent!', mail_address=newmail)) def send_passwordreset(user, new=False): - expired_tokens = PasswordToken.query.filter(PasswordToken.created < (datetime.datetime.now() - datetime.timedelta(days=2))).all() - duplicate_tokens = PasswordToken.query.filter(PasswordToken.loginname == user.loginname).all() - for i in expired_tokens + duplicate_tokens: - db.session.delete(i) - token = PasswordToken() - token.loginname = user.loginname + PasswordToken.query.filter(db.or_(PasswordToken.created < (datetime.datetime.now() - datetime.timedelta(days=2)), + PasswordToken.user == user)).delete() + token = PasswordToken(user=user) db.session.add(token) db.session.commit() diff --git a/uffd/session/models.py b/uffd/session/models.py index 70d38fb9..a64dd5cd 100644 --- a/uffd/session/models.py +++ b/uffd/session/models.py @@ -6,9 +6,7 @@ from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Enum from sqlalchemy.orm import relationship from sqlalchemy.ext.hybrid import hybrid_property -from uffd.ldapalchemy.dbutils import DBRelationship from uffd.database import db -from uffd.user.models import User from uffd.utils import token_typeable # Device login provides a convenient and secure way to log into SSO-enabled @@ -115,10 +113,11 @@ class DeviceLoginConfirmation(db.Model): id = Column(Integer(), primary_key=True, autoincrement=True) initiation_id = Column(Integer(), ForeignKey('device_login_initiation.id', - name='fk_device_login_confirmation_initiation_id_'), nullable=False) + name='fk_device_login_confirmation_initiation_id_', + onupdate='CASCADE', ondelete='CASCADE'), nullable=False) initiation = relationship('DeviceLoginInitiation', back_populates='confirmations') - user_dn = Column(String(128), nullable=False, unique=True) - user = DBRelationship('user_dn', User) + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False, unique=True) + user = relationship('User') code0 = Column(String(32), nullable=False, default=lambda: token_typeable(1)) code1 = Column(String(32), nullable=False, default=lambda: token_typeable(1)) diff --git a/uffd/session/views.py b/uffd/session/views.py index e5589ee4..e09fac3f 100644 --- a/uffd/session/views.py +++ b/uffd/session/views.py @@ -9,7 +9,6 @@ from uffd.database import db from uffd.csrf import csrf_protect from uffd.secure_redirect import secure_local_redirect from uffd.user.models import User -from uffd.ldap import ldap, test_user_bind, LDAPInvalidDnError, LDAPBindError, LDAPPasswordIsMandatoryError, LDAPSASLPrepError from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay from uffd.session.models import DeviceLoginInitiation, DeviceLoginConfirmation @@ -21,13 +20,13 @@ login_ratelimit = Ratelimit('login', 1*60, 3) def set_request_user(): request.user = None request.user_pre_mfa = None - if 'user_dn' not in session: + if 'user_id' not in session: return if 'logintime' not in session: return if datetime.datetime.now().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']: return - user = User.query.get(session['user_dn']) + user = User.query.get(session['user_id']) if not user or not user.is_in_group(current_app.config['ACL_ACCESS_GROUP']): return request.user_pre_mfa = user @@ -35,30 +34,10 @@ def set_request_user(): request.user = user def login_get_user(loginname, password): - dn = User(loginname=loginname).dn - - # If we use a service connection, test user bind seperately - if not current_app.config['LDAP_SERVICE_USER_BIND'] or current_app.config.get('LDAP_SERVICE_MOCK', False): - if not test_user_bind(dn, password): - return None - # If we use a user connection, just create the connection normally - else: - # ldap.get_connection gets the credentials from the session, so set it here initially - session['user_dn'] = dn - session['user_pw'] = password - try: - ldap.get_connection() - except (LDAPBindError, LDAPPasswordIsMandatoryError, LDAPSASLPrepError): - session.clear() - return None - - try: - user = User.query.get(dn) - if user: - return user - except LDAPInvalidDnError: - pass - return None + user = User.query.filter_by(loginname=loginname).one_or_none() + if user is None or not user.check_password(password): + return None + return user @bp.route("/logout") def logout(): @@ -68,12 +47,9 @@ def logout(): session.clear() return resp -def set_session(user, password='', skip_mfa=False): +def set_session(user, skip_mfa=False): session.clear() - session['user_dn'] = user.dn - # only save the password if we use a user connection - if password and current_app.config['LDAP_SERVICE_USER_BIND']: - session['user_pw'] = password + session['user_id'] = user.id session['logintime'] = datetime.datetime.now().timestamp() session['_csrf_token'] = secrets.token_hex(128) if skip_mfa: @@ -105,7 +81,7 @@ def login(): if not user.is_in_group(current_app.config['ACL_ACCESS_GROUP']): flash(_('You do not have access to this service')) return render_template('session/login.html', ref=request.values.get('ref')) - set_session(user, password=password) + set_session(user) return redirect(url_for('mfa.auth', ref=request.values.get('ref', url_for('index')))) def login_required_pre_mfa(no_redirect=False): @@ -181,7 +157,7 @@ def deviceauth(): @login_required() @csrf_protect(blueprint=bp) def deviceauth_submit(): - DeviceLoginConfirmation.query.filter_by(user_dn=request.user.dn).delete() + DeviceLoginConfirmation.query.filter_by(user=request.user).delete() initiation = DeviceLoginInitiation.query.filter_by(code=request.form['initiation-code']).one_or_none() if initiation is None or initiation.expired: flash(_('Invalid initiation code')) @@ -194,6 +170,6 @@ def deviceauth_submit(): @bp.route("/device/finish", methods=['GET', 'POST']) @login_required() def deviceauth_finish(): - DeviceLoginConfirmation.query.filter_by(user_dn=request.user.dn).delete() + DeviceLoginConfirmation.query.filter_by(user=request.user).delete() db.session.commit() return redirect(url_for('index')) diff --git a/uffd/signup/models.py b/uffd/signup/models.py index 09483738..e089b73f 100644 --- a/uffd/signup/models.py +++ b/uffd/signup/models.py @@ -1,11 +1,10 @@ import datetime from crypt import crypt -from sqlalchemy import Column, String, Text, DateTime, Integer +from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey +from sqlalchemy.orm import relationship, backref -from uffd.ldapalchemy.dbutils import DBRelationship from uffd.database import db -from uffd.ldap import ldap from uffd.user.models import User from uffd.utils import token_urlfriendly @@ -33,8 +32,8 @@ class Signup(db.Model): displayname = Column(Text) mail = Column(Text) pwhash = Column(Text) - user_dn = Column(String(128)) # Set after successful confirmation - user = DBRelationship('user_dn', User, backref='signups') + user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=True, unique=True) + user = relationship('User', backref=backref('signups', cascade='all, delete-orphan')) type = Column(String(50)) __mapper_args__ = { @@ -58,7 +57,7 @@ class Signup(db.Model): @property def completed(self): - return self.user_dn is not None + return self.user is not None def validate(self): # pylint: disable=too-many-return-statements '''Return whether the signup request is valid and Signup.finish is likely to succeed @@ -98,8 +97,8 @@ class Signup(db.Model): if User.query.filter_by(loginname=self.loginname).all(): return None, 'A user with this login name already exists' user = User(loginname=self.loginname, displayname=self.displayname, mail=self.mail, password=password) - ldap.session.add(user) - user.update_groups() + db.session.add(user) + user.update_groups() # pylint: disable=no-member self.user = user self.loginname = None self.displayname = None diff --git a/uffd/signup/views.py b/uffd/signup/views.py index 09b82d39..d5c3ddcf 100644 --- a/uffd/signup/views.py +++ b/uffd/signup/views.py @@ -6,7 +6,6 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash, from flask_babel import gettext as _ from uffd.database import db -from uffd.ldap import ldap from uffd.session import set_session from uffd.user.models import User from uffd.sendmail import sendmail @@ -113,7 +112,6 @@ def signup_confirm_submit(signup_id, token): if user is None: return render_template('signup/confirm.html', signup=signup, error=msg) db.session.commit() - ldap.session.commit() - set_session(user, password=request.form['password'], skip_mfa=True) + set_session(user, skip_mfa=True) flash(_('Your account was successfully created')) return redirect(url_for('selfservice.index')) diff --git a/uffd/translations/de/LC_MESSAGES/messages.mo b/uffd/translations/de/LC_MESSAGES/messages.mo index 9a19ad476f876ce75968e81163d0743da932f670..ba3b019d4866c4a53f286272f0989265dd3ab66e 100644 GIT binary patch delta 5264 zcmcchjj`b;WBolLmZ=O33=C$B3=A?13=C&DK|BPm5oTcEXJBBc6=q-%W?*3G6=q;y zV_;xdD9pg%!@$6>LYRSphk=3Nt1trt7Xt$WlL&;*D+1w5i7+tmGB7ZxiZC#+GcYh1 zi!d;VFfcG!i!d-KGBDIL1c)#&*fTIN6p1h}7&0(0Y!YE$C}dz@cmmZJCd$AN&A`Br zBFez9oPmMifG7ilI|Bnlh8P1wEdv9?1~CQ(B?bltJ#hvGHwFfVKyd~JNd^Xn8R85K zVj%OxAs#p>&cI;7z`$@*9O5t`36MwX85m^23<d@j35Y^%2?hoSkP9Uk7+4t?7;2z& zlLW+p9Z+?<Bp4WE85kH&NiZ;oGB7Z_kYHetW?*3WC&9oV$iToLCCR|R!N9<vBMEVc zsU#!>eIywe_!t-%5+oTIxa%1h81f|{E~<uVXp;oFjDewF5)yRNBq0_ok%R>8E|5kB z28QF3kf6K_6@LMxzd|iyl44-6WME(rlY*ERAO+DME(H#Hh7>6VhI)_-+n^HtQV@&g zNHH*|gMv<qfkBFaf#H@E149ZZj-m2V(hwgMK<RpE1_lKN28KRq1_o;e28Q+0kSKjE z&A^b!z`&p_1Bv^oGLR6ND+6)RG8u+?1_1^JhP5(~px-CMz`)JGz;I56fq@xhu?!?g z@5wMQ$T2W5{E~rKASnw8nNV4Xfyq#Q4wSBvWnj=`U|{Hwg;=yn7UJNYvJ4DY85kH& z$TBcgFfcI8ldET7n8?7uATAHFXr(+v!%=w#24zrgkcT+zgFM6lkT*fOfnNcV=;Rd` z7&bF7FgPhd)O}Nc_>@f%k_h<}85qPF7#K7ZA#v}Z$N=_m2$Y{%uL#L@m5LCHrzt{0 zU@la_N=1mpdleyZc195rwC@xl7U?NL^qDC!FlaL{FxV<V5^bgu!~ywA3=Av`3=Ex0 z3=E*0-KPYJsu@ZQ45|za4D~yeAVGUyiGjfdlzNpQaqp}Q34v^7h=KjekVHCF86v+x z8DhW*Wd;U$1_p+A$`A|rR3Hx0P+?%uU|?X-SAkd<p#pJ8r3xew)~SF)te&A;1(N7y zt1vL=GB7Y~glf320`WP>nV^D$Ulrm2aaFKJ1|=xpNEH%dwyF>xda5!o=rb@d1gk<U zY*uAp2nD4DsQP=VkhJwu737e528Op_1_Q${RY=^ht1&PLF)%Pls6j$NTMc5czZ%4+ zk!lPKDhvz^m1>aeH&2a$VG08S!)7%GhK&pi3?Ax`MEMI!GiyN1<<Wqo0a*=5;xyD? zU=Rc4|7fT{kp{#kb2LCMV_;aJ0g3Ca8Vn5SpzH^gkJW_4eWoVFz(P$(NVRG*Fyu2Z zFig>8U}$4tU|`XL<dSYJh=XQoK?<TpQ1P8w3=H+Qpn^mT5@)R1kf7z!hWJ=U8)C4U zHYAOhYBMlsF)%QMK;`STAr^E(`O~!_X=R}{q~KYu4Jkh!YD3b(A8kmKsOmuM)6!w6 zXNX{6U@+8y_`FMpfkA?Sfnk~sBm~y$K!SLW4#Wo+bs!G91(koP!@w|ufq~&0RK8yq z;-DG2kRV^C3(>z_7h>TVT}T?btP6?ad%E?IAo-{Zsm(a_AO%CH9s}56=kypDCNVHD zsOv)v*rgAt0}kplFtjo-Fnrc$V3+_(3kHzX&t(XS5*0%RhQ$mF3>JnA3;_%b40jA6 zxj@&5fx&=*fx*uRk_PJQjUYj_(g;$kJvM?Eq-_k*;A+gk;K;zhkY)_2lvWr+66H%{ zNKo>cK!RMz1d@v+Oc)s6f@(t(NaZAB3Q6r&rjQV^GlhhpzbV8+^>L;Q40#L;45_9J z3?2*&3|CAU7`zx57-Y>Li7dm6fx(7>fuX<*5+!TRAQtX2gGAW@GibqN265OsGe}wy zGKW}TXb$m^1(Y9T4oTFJ<_ru`p!$ENIi!C70;K~iAVI#yf`K8Gfq}u^l7YdDfq`ME zB?ChYC@3u%7+e_`7%Z(IC0@N11H&-}28PvE5RVjDL)rlktr-|fKt-<&1A{RG1H)V! zh|e$DKtl9^4X6mOXJAmWWnfTaU|?{yg`|NTTLuOr1_p+mwh)Wn*g{G=BRfbP5n;!` zP|Co-P+|uu+h5s1Qa6)5qyW>lXJCkAU|@)}XJANVU|`q`r9~Ye=9D`yFdP9n$bo@j zDk%RaIYLtH4@XE)u{lA4R>28kp^g(Iu05O}xxm*6l8VEe7#Nrs7#I?rAW@Y9rE{Dh z*|@}sfuWa?fuY3-lG=q_Ac<4e1!9h+3j;$vC`g@LAc<qX3&i3LE)WOpae-KL$^~M; zRToIyzi@#hws$U&@_@w^;$wYR1_ozPW#h`g5XZp4aMl%){dC+I7?K$n7#!Ug7}kKI z(hU+tLGBFo3`GnK3^Uy!Wj2oo14A$a1A~nR#OD({AQr9hfH>fg2LppC0|UcV4~WCK zJs|~^m?tCz6g(jw(eQ*=VCe}-v<;pR^}U{u#5cneQiR{|1jRoC1H)fWP-0+UQ1F73 zSk7LMHe8$+Bo!BUK|-V)D&Oh_@!0||NTS*brH^<)9CX<W62y1CAP#!w1*zu0L(LWO zhB!>E-Wy_&F;u_<N{4zwLLkW-QZCeaLwtD58{)8Y-jIUifj1<|1bra1tPi9>)AV6r z&|_d=aPWbY1NlCXN~+ffQi&<~LefILlP|;vVZICuI-qjE7m__D`9h-NiZ7&@t>Fg= z!exFCmv8igIB1_A#Gqq-kSMz42T6Qy{U8O;Uq49hV)TdP4h??>h7bk@hA@AyN9q|? z`$G)g>krX*!XIMb4Sz^`{f$4wBGmv$Q0fLSFuY=5V6Y2dV5nqZV9*R?U|7w-z_2(F zQZ)MqK|-Q22vTnJ20<KlBnWIF!}TCY6uk@rm9X^;44;D-7!ENoFo*<09B@4tqTqQj zB<MZ{LlPl>2qbN&hCmE-2!XVOf<qutQ5^ya$u&@VH&p$p5QvAKhCs@dpCOPsK_`@f zfghCrYeOL}?hR#N09D5`Lm>{h9tugN4?`KiZ8)|tNTT8ngCtgwFo*-p!yvgQJ`9rW z`okC){6I~zFpv)!7;M8K`r^VN_N0eHa#3+O14BJ1k*o}d#QlwMh>uw#AR3e-Ac;sP z0%AdQ1jL8kP<mDbB$2L+fTZrD5ey7g3=9m9A|P=s8wm+=??^}#$3p20sCaQCLp`|h z*cu6O>C#9@P#ueewDWI8LVPY31u6TbqaZ<F5d}$H-BAz+^+!S44fCM#+oK>w^QkCE z(flY1l4!Z3At5Re4RN4tG_?NDj%Hw3$-uzS6wSbJ0MuuTfw=f>3?w9e#6T=&jD=(` zkyuE`XvacQcT_B-dTxw`IOtF;BrRQxh19AKV<DB1RvaV|&yRzYD;w&e0^8#tE<O>* zz|h0M!0;fBfngm314C{+BnV{^AU@DbfFwfe1c*gp2@DKQplq7}Ni!=FAmz!X1c<)l z2@nV0On}su9}*zdxlAI&gY}V#kVKIJ6(~rAxU4!65(Q0(khtBK$iQI2z`$@j5n_=* z5`>mYVqnN+U|>*5f|xTY2~x|=OM-;Jkt9geTuFjt<F`qWko*dfuV>&$W?)zaYHlY( zirB-+5FcDjhG=}245=0WCquHKWD3M*`Y8~L98(~1nw|nlBRwe$46_&*7+$783a*w^ zNE?wU4dSucX^_OeD2;((CPO^~!}&BwgCjE?lCA!vLxNU515z&NWk8zMA2T4eU1BDr zr__@PX~SL2gw%S!GZ`3yKn<2GNG_U`1+i#D76XGf0|Ud|ECvQoP=S>VDF=$P85o#B z`F~k9B$cnqhWK!OHUonQBLl<HY)I<8l><rb%()PQ*>fT4RC6I|!aA3MAsf{3$Yo%# zV_;y|k_)lub1o!~ZSxoy0zvY5kdU2~2T9cH^B5TFLEY^gc@Uo*$%CZocX^O}Et?Oq zz%n0_J>Bvl2BzmjDy5=)25|MgE1v<}&(|%0w4SFIFfeRjU|<j^gcL{z3nA69LJ<Q) z6$1l9dJ)7ykBS)T!5s>dVg`m!3=9nM#gL{|Pzktk$Z(>BfuVzefkC+xlK9q?LgM~m zDWo3%Tnb5?#$^x>1e7r__%bjsRFy%>{u5;ohyE{v<ahUSNC=jegW{fnVRCsr#HYFy zkXp~P0%A~i1;l4tD<GAOMkR#5s1jnKXcYs)X9flaqbf-MU#%MAuv^uT?D?h|5_0S{ zkk+tB4J3+<YaqGA0!q6mLL~xfAaN2}1IZ?FHIP)FR<l`4ScQ!>y(qu5V6&^_QDzSX zPZtJv5LY3&C^fMpH3iBmEl5G)m1mY@D3oMm7Axc>=B6s-7b#?>C?w_-r6#6SDx_9q z7ME;hmNi%9Ff>&#G_W!?+wA1@on64asI;IURUth!=kT`T<cyNd7GCFA5+F8$Wm1cZ zOHy-kN*JIj6SGT76LT_)GgFJ;V!4?m3MrYX#i_Xpeu=rMc?$U{sYMWxA_Y$ui0vQ^ S3Q4I&shK6koB#R6=l}qRF`u{q delta 4990 zcmZqp$$0A<WBolLmZ=O33=D>h3=A?13=B&+K|BQ35oTcEXJBB^6=q-%W?*2j6=q;y zV_;wi6lP%XVPIg05N2TDVPIfbD$KyZ#lXO@3CiCK<)0E}VBlq7V7MyGz`)MH!0=d@ zfkA|Uf#J0<1A`(1Lp_*h&%nT-B*MU8$iTpmB*MT@$iTobMFgUeO_YHlnt_2qM3jMH zIRgVjfhYrmI|BoQgct)uEdv8Xf*1pX5(5LnJuwCbHwFd<MsWrPNd^W64{-(tF_3xU z5D!#}GcZ^%FfeqALmYZY9ORLD28Od>1_Q$tsKVRg3=9q+7m715ure?(=tw|lBMFED zEhHcYW=SwG$TBc6R7o%}h%zuR%#dJUkY-?DSSP{2AjrVLa7u!KfrEj8;f@5vAx|YB zA^1mvfq{>Kfk8l$fq}c8fq_9@65=9lNr(nBNs!AJ80;k>2D?c@f;a}Gfq{V`QxY6# z45d)<E+{=+5@Nv;Nd^W>1_p*5P;)*(<$p?ogPeg)3Sy6j6azy&DDDiUAQst4F)*kz zFffEjF)&CmFfi0eF)*YsFfdGq%KwpqxLimYLMuu$FeorEFc?TPFj#}qgfs&KC@OoU z85lAd7#Pk<L*m?A24bPT48&s|G7Jm?3=9nZG7R<L#FZe!z`zZPTNwrhW{|})kf3ak zVPKGBU|^Ud1F>MY3?xl_hZ@8x3vmdKECa(;1_lOcSq6p*1_p+YvJ4Cp85kI{<RIq! zk%L$)A<w{|3<`01NEABD*Fy{lh6*IfLlRSwJOjgK1_p+yP;pNMh)*IFAc-qcfq_As zfq|h~0TSes6(BxZ4(0DqfaIn#3J{B5DnLB=0V@AT0b;K}y&@z|<P{-7=cov=s8JE3 zu~U(OL7RbrVS*wg5$;ihIN*>X0|N^vaVat|fO70ZMMzY<R%BpMWnf_7QG$e=krD%g z3n*%oAW=I_2@(SPlpyBSKT?7uz86phpOqj6NGmfi$TKi7I4VOdOjKq7=lg191_liV z28JeOh=r?_Ar3jC3`wLHl_5cUPZ^S?-YGLM=rS-cu&6-P8>v8i9;^cPXgx!c3d8|f zDiDpOP=2clB*-SHKzukyg@Hkzfq`L}3dF)|Dhv#v3=9mnq3R7)A!)}(72*&FDDA5X ziJB-?1_mJp28L`^NC?!ag3PUFU|6II@#z{>1_l)d28J`LknHtQm4RUj0|Nt_8Uw>d z1_p-NYLHays}7;V)FB4Pt3%R2fjR?&CIbUQi#jB~uY>ZBszW^TULE4F-|CR4=FnhZ zNC)MA4-JTd4I1DeVc4SqG4O~6B&cp^FfimZFfcsVU|?utU|<N>gye#Inh=M)(S%s| z6)Mi7#lT?8z`!7-1&Oi<El9}5Yk_>sz>u#6F}G5SfuSChy*jiQ7_=A|7?wj7T+)JA za0kkNr3FbWU$h_v%P%cR`Cy_ANelkkkSM9phFDmu&A<==3JGn9$L?w~Fi0>kFuc@; zgaDHcB!u~O80x`Aq>2v2MY=i=1vWYi3^N!Q7(8_#K76DDanNfWNRa=88o;Fsu~1$Y zl7`fDA#rS|3keYyT}UMrtqUm^R_H<;rl`ljFbPz2>(xUH;MIqe?IQXN46O_d3~u@i z3==?UK_8O(V+|ltQf|P&u$X~?q1%9gA%KB_LEjLPsv8U$7z`K~7#12r(!eD{NJ#xL zgj7~$Mi6uAj3DY~7%?z7GB7agtT%#GHouJ^snW(65|jzXkRVSrhGe5`V+Mw|3=9m7 z#*j)U-vpA{drcrAGSLJQf{RQb9@=Qaz>o*3156kgJQx@l)J+)}yg((aDI|^UHf3P2 zVPIgWKWqvKat1Sqg}i2vI1@I57EERkhdG)-3Y1hchy^WX5Fd3z`Af|psd|kW149%8 z1H&6LNVV;54xtyDLxTLjIRir~0|Ub(3kHyt^$b5O7#LzeL21dr;L5<j&|?WH@h(|1 zFdSoGVEAVV@ySsuNd0bN&A?CsDtfIM7>pSh7(Q4-e6C^x2~uMlND*FY!@!^hs+Mga zX<)w%1A`H$WVD6YV{Z#8=;|35T5Tbf#A;gxhEfIwhU2!7vfb7WlDb3fASGCx9Rouo z0|UbvI|hbCP`O|ap)>431~D+4vS(m80&<W&0|Tf@wbcQVXuTaFAr<Ke3E5&th<){r z3=Axw{6E_flFAo2LQ?TcM+OEa1_p*Lj*z(84yE@yLh|u(M+SyoMh1rKj*!%z>I_Mo z70wWIdYmC4I@KAHHa<B+EM|6rIEdc`Vvno~14BKiNYrqF#J#l(B(XWVK+1z~7l@CW zTo@RfL6waQ14A4G1A~GqB>&dCGB6~Anrf~L3~Lw|7=E}yqG+ib149ug4Y)yy?09zu zhF}H;hJJU5&mX%(?D_A`P!BGXMLif8Oc@v$G&~?Ki}Qe#RGA)-5GeM5_@vqcVnL4w zB+*`ms(;`CNqnz8AVs*gCnOC7K<Q#nNP#uYlL6c`+vo{N#D_iWAwhBqs^Eqv#Aly9 zA&H8^3qp%~K^&y!1qosUFG&4v=>;(`%nRb63@?cOQYgO*N>BHK_<V^Mq&zs_1@Rb1 zy*I=F0dGhFqT~&Ut6(S{?+q!*vb`A?^gz|FHzfaW^oCSU=e;47SBeiL4Yc?`96ZB^ zfk6jUr29Z}#T6e&6o~mkTFqI$kdUr_;R|v3XJ3em82lgxari+Jmy91I(dqj^3LJYs zNVanJgXD@VKL&;nP&V^}c;u}g#GL<rkSJsKXJB{*YOeb;FjO)yFfa!&Fsx=^V5kiM zm$>x|nt>1>M+brn1cvlLh{f{)AqH*?garNJKuFPfHjshg5Ca3lyFiG;HU>fD4+cR( z=5!DwalH(J#QFaqh<Q@MkQR_`FeIcygFzux&%n?IW-u^J4Tfk~5)27}{lSm|<8m;h ztY-^>SQs7xad3JFq?#=bfjD4e2qf|C4q*T{%^rk6(#q2iNMd{!0&##~C?r={hB7e3 zf%1Q5C<7?e8BT;kd?+3U(P$9{vB)tDk~@6EAZegE3=+4S!XQ4r4^{scD$f=UvA{GO z<U<CAR482%4oQT~;gG~TKb(P~-im>NVNW<DF29FEf?PEMk~qwvv{M8`+&2P}*y1A~ z4y})Xgw%ovNb`J41jOebA|M6O*9b_^2Sq~CPHH5?L79<|_CQ4>H2+VGgcP|;A|XZY zo=8Zdd>RP}(oc~P2Z~2Qa)WCW1H(!N28Ng@28IKmqBt7j;N#Jdkh>HOvG{g0ME$#H zNXW6qKoYM>45ZeLj)AuS=fptbc6AJ-y4)QDsbpAUA*r}B7E)kzLHQG7Ar4*?%fQgX zz`(F8mVsd%0|SG593-T^#X&s45f4dIqVW))8^kj(I599V*vCWCOjCV4q`>HjhiF_F z4{`D4ct|aHDjrf#e~X9s&^Q5-ChQU*e6IwE!$K1vQ4o^=iQ5?o3=Ad=3=CTnAQruX z(q9r77;-_iD%70%oJ2_NRgnk@fq99LxLKD7$;ZbNAwhW_D*q^vfngN`1H-38NXa=j z3F3j(Nf3Ppk|4F+%_K-R{G0^w7-urX9_eIAlsYDZ(?~r-S~3H}ECvRK!^x16DJ})l z_Pdh;@mXmqB(>M1GBC_!U|?983Ta3<r$MsSwKPc3{!D|E3moZ?*7E6eNF`>S0qFsy zWk8x{t1}>#+0_gNh9J=RgJdQo8|7p|Eb7W+VDM&OVA!6?z~ITiz`&dZDF=MBAgQ<^ z3zEuPvLHU}%wk{=VPs&Kp9M+0Te2bQ?q);GeV7eV_dgqwCPZ@>7_vbF9ytsQcA)&< zmjkirYz`z&#d8@L0znFLAwgS`3rW?TxsbS@lne36yj)1CK9LK_*WYs?76|7-vZq2G z#5~75NTuVG2dS2)<UzXU?D-4~^`P!@VLk)H1_lO(SNV|oeRct)n*CM4z)%J10~SIY zw5Jdfq<lpT44)Vn7%Yn*Ehe2}NSkj_F#|&f0|Ud~Vo2g^D}luQ?h;7#e6|FVIC)DU z9?&XfsAup64LFoS%Kk;A5SQL8h2(d|GDr~mmqFq_w+!M__HszArd$p&D775ov;J~O zCBsw!;n!3^EPP+V!0;K=_N#<+>p{K(4bg0=3V`I#V^xr#dsqc&<-V(e#4&F*B*=xJ zv|KerT&o%qCHmEnTw+lTN%aoZo2!IX*fy_`+{3(iyR3mKhk>brfuWUw(dPF~-`O{R N_d3h6StlSw2LJ`@R~rBT diff --git a/uffd/translations/de/LC_MESSAGES/messages.po b/uffd/translations/de/LC_MESSAGES/messages.po index a1673d29..4568ca95 100644 --- a/uffd/translations/de/LC_MESSAGES/messages.po +++ b/uffd/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-09-05 01:02+0200\n" +"POT-Creation-Date: 2021-09-15 10:56+0200\n" "PO-Revision-Date: 2021-05-25 21:18+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language: de\n" @@ -62,42 +62,42 @@ msgstr "" "Einladungslink muss entweder Account-Registrierung erlauben oder Rollen " "vergeben" -#: uffd/invite/views.py:109 uffd/invite/views.py:138 +#: uffd/invite/views.py:122 uffd/invite/views.py:157 msgid "Invalid invite link" msgstr "Ungültiger Einladungslink" -#: uffd/invite/views.py:126 +#: uffd/invite/views.py:140 msgid "Roles successfully updated" msgstr "Rollen erfolgreich geändert" -#: uffd/invite/views.py:141 +#: uffd/invite/views.py:160 msgid "Invite link does not allow signup" msgstr "Einladungslink erlaubt keine Account-Registrierung" -#: uffd/invite/views.py:163 uffd/selfservice/views.py:53 -#: uffd/signup/views.py:49 +#: uffd/invite/views.py:186 uffd/selfservice/views.py:50 +#: uffd/signup/views.py:50 msgid "Passwords do not match" msgstr "Die Passwörter stimmen nicht überein" -#: uffd/invite/views.py:168 uffd/signup/views.py:54 +#: uffd/invite/views.py:191 uffd/signup/views.py:55 #, python-format msgid "Too many signup requests with this mail address! Please wait %(delay)s." msgstr "" "Zu viele Account-Registrierungen mit dieser E-Mail-Adresse! Bitte warte " "%(delay)s." -#: uffd/invite/views.py:170 uffd/signup/views.py:56 uffd/signup/views.py:92 +#: uffd/invite/views.py:193 uffd/signup/views.py:57 uffd/signup/views.py:106 #, python-format msgid "Too many requests! Please wait %(delay)s." msgstr "Zu viele Anfragen! Bitte warte %(delay)s." -#: uffd/invite/views.py:183 uffd/signup/views.py:68 +#: uffd/invite/views.py:206 uffd/signup/views.py:69 msgid "Cound not send mail" msgstr "Mailversand fehlgeschlagen" #: uffd/invite/templates/invite/list.html:6 #: uffd/mail/templates/mail/list.html:8 uffd/role/templates/role/list.html:8 -#: uffd/user/templates/user/list.html:8 +#: uffd/user/templates/group/list.html:8 uffd/user/templates/user/list.html:8 msgid "New" msgstr "Neu" @@ -129,107 +129,107 @@ msgstr "Link kopieren" msgid "Show link as QR code" msgstr "Link als QR-Code anzeigen" -#: uffd/invite/templates/invite/list.html:42 +#: uffd/invite/templates/invite/list.html:40 msgid "Signup" msgstr "Account-Registrierung" -#: uffd/invite/templates/invite/list.html:51 +#: uffd/invite/templates/invite/list.html:49 msgid "Disabled" msgstr "Deaktiviert" -#: uffd/invite/templates/invite/list.html:53 +#: uffd/invite/templates/invite/list.html:51 msgid "Voided" msgstr "Verbraucht" -#: uffd/invite/templates/invite/list.html:55 +#: uffd/invite/templates/invite/list.html:53 msgid "Expired" msgstr "Abgelaufen" -#: uffd/invite/templates/invite/list.html:57 +#: uffd/invite/templates/invite/list.html:55 msgid "Invalid, unpermitted creator" msgstr "Ungültig, erstellt durch unberechtigten Account" -#: uffd/invite/templates/invite/list.html:59 +#: uffd/invite/templates/invite/list.html:57 msgid "Invalid" msgstr "Ungültig" -#: uffd/invite/templates/invite/list.html:61 +#: uffd/invite/templates/invite/list.html:59 #, python-format msgid "Valid once, expires %(expiry_date)s" msgstr "Einmal verwendbar, gültig bis %(expiry_date)s" -#: uffd/invite/templates/invite/list.html:63 +#: uffd/invite/templates/invite/list.html:61 #, python-format msgid "Valid, expires %(expiry_date)s" msgstr "Gültig bis %(expiry_date)s" -#: uffd/invite/templates/invite/list.html:80 +#: uffd/invite/templates/invite/list.html:78 msgid "Invite Link Details" msgstr "Details zum Einladungslink" -#: uffd/invite/templates/invite/list.html:87 +#: uffd/invite/templates/invite/list.html:85 msgid "Type:" msgstr "Typ:" -#: uffd/invite/templates/invite/list.html:87 +#: uffd/invite/templates/invite/list.html:85 msgid "Single-use" msgstr "Einmal verwendbar" -#: uffd/invite/templates/invite/list.html:87 +#: uffd/invite/templates/invite/list.html:85 #: uffd/invite/templates/invite/new.html:9 msgid "Multi-use" msgstr "Mehrfach verwendbar" -#: uffd/invite/templates/invite/list.html:88 +#: uffd/invite/templates/invite/list.html:86 msgid "Created:" msgstr "Erstellt:" -#: uffd/invite/templates/invite/list.html:89 +#: uffd/invite/templates/invite/list.html:87 msgid "Expires:" msgstr "Ablaufdatum:" -#: uffd/invite/templates/invite/list.html:90 +#: uffd/invite/templates/invite/list.html:88 msgid "Permissions:" msgstr "Berechtigungen:" -#: uffd/invite/templates/invite/list.html:93 +#: uffd/invite/templates/invite/list.html:91 #: uffd/invite/templates/invite/new.html:21 msgid "Link allows account registration" msgstr "Link erlaubt Account-Registrierung" -#: uffd/invite/templates/invite/list.html:95 +#: uffd/invite/templates/invite/list.html:93 #: uffd/invite/templates/invite/new.html:22 msgid "No account registration allowed" msgstr "Keine Account-Registrierung möglich" -#: uffd/invite/templates/invite/list.html:98 +#: uffd/invite/templates/invite/list.html:96 #, python-format msgid "Link grants users the role \"%(name)s\"" msgstr "Link gibt Accounts die Rolle \"%(name)s\"" -#: uffd/invite/templates/invite/list.html:104 +#: uffd/invite/templates/invite/list.html:102 msgid "Never used" msgstr "Keine Verwendungen" -#: uffd/invite/templates/invite/list.html:108 +#: uffd/invite/templates/invite/list.html:106 #, python-format msgid "Registration of user <a href=\"%(user_url)s\">%(user_name)s</a>" msgstr "Account-Registrierung von <a href=\"%(user_url)s\">%(user_name)s</a>" -#: uffd/invite/templates/invite/list.html:111 +#: uffd/invite/templates/invite/list.html:109 #, python-format msgid "Roles granted to <a href=\"%(user_url)s\">%(user_name)s</a>" msgstr "Rollen an <a href=\"%(user_url)s\">%(user_name)s</a> vergeben" -#: uffd/invite/templates/invite/list.html:122 +#: uffd/invite/templates/invite/list.html:120 msgid "Disable Link" msgstr "Link deaktivieren" -#: uffd/invite/templates/invite/list.html:126 +#: uffd/invite/templates/invite/list.html:124 msgid "Reenable Link" msgstr "Link reaktivieren" -#: uffd/invite/templates/invite/list.html:140 +#: uffd/invite/templates/invite/list.html:138 msgid "Invite" msgstr "Einladungslink" @@ -251,7 +251,7 @@ msgid "Must be within the next %(max_valid_days)d days" msgstr "Muss innerhalb der nächsten %(max_valid_days)d Tage liegen" #: uffd/invite/templates/invite/new.html:19 -#: uffd/signup/templates/signup/start.html:11 +#: uffd/signup/templates/signup/start.html:6 msgid "Account Registration" msgstr "Account-Registrierung" @@ -268,8 +268,8 @@ msgstr "Enthaltene Rollen" #: uffd/rolemod/templates/rolemod/list.html:9 #: uffd/rolemod/templates/rolemod/show.html:46 #: uffd/selfservice/templates/selfservice/self.html:101 -#: uffd/user/templates/group/list.html:10 -#: uffd/user/templates/group/show.html:11 uffd/user/templates/user/show.html:95 +#: uffd/user/templates/group/list.html:15 +#: uffd/user/templates/group/show.html:26 uffd/user/templates/user/show.html:95 #: uffd/user/templates/user/show.html:127 msgid "Name" msgstr "Name" @@ -279,7 +279,8 @@ msgstr "Name" #: uffd/rolemod/templates/rolemod/list.html:10 #: uffd/rolemod/templates/rolemod/show.html:28 #: uffd/selfservice/templates/selfservice/self.html:102 -#: uffd/user/templates/group/list.html:11 uffd/user/templates/user/show.html:96 +#: uffd/user/templates/group/list.html:16 +#: uffd/user/templates/group/show.html:30 uffd/user/templates/user/show.html:96 #: uffd/user/templates/user/show.html:128 msgid "Description" msgstr "Beschreibung" @@ -289,26 +290,26 @@ msgid "Create Link" msgstr "Link erstellen" #: uffd/invite/templates/invite/new.html:56 -#: uffd/mail/templates/mail/show.html:28 uffd/mfa/templates/mfa/auth.html:39 +#: uffd/mail/templates/mail/show.html:28 uffd/mfa/templates/mfa/auth.html:33 #: uffd/role/templates/role/show.html:14 #: uffd/rolemod/templates/rolemod/show.html:11 -#: uffd/session/templates/session/deviceauth.html:44 -#: uffd/session/templates/session/deviceauth.html:54 -#: uffd/session/templates/session/devicelogin.html:34 -#: uffd/user/templates/user/show.html:8 +#: uffd/session/templates/session/deviceauth.html:39 +#: uffd/session/templates/session/deviceauth.html:49 +#: uffd/session/templates/session/devicelogin.html:29 +#: uffd/user/templates/group/show.html:9 uffd/user/templates/user/show.html:8 msgid "Cancel" msgstr "Abbrechen" -#: uffd/invite/templates/invite/use.html:11 +#: uffd/invite/templates/invite/use.html:5 msgid "Invite Link" msgstr "Einladungslink" -#: uffd/invite/templates/invite/use.html:14 +#: uffd/invite/templates/invite/use.html:8 #, python-format msgid "Welcome to the %(org_name)s Single-Sign-On!" msgstr "Willkommen im %(org_name)s Single-Sign-On!" -#: uffd/invite/templates/invite/use.html:18 +#: uffd/invite/templates/invite/use.html:12 msgid "" "With this link you can register a new user account with the following " "roles or add the roles to an existing account:" @@ -316,33 +317,33 @@ msgstr "" "Mit diesem Link kannst du einen Account mit den folgenden Rollen " "erstellen oder diese Rollen zu einem existierenden Account hinzufügen:" -#: uffd/invite/templates/invite/use.html:20 +#: uffd/invite/templates/invite/use.html:14 msgid "With this link you can add the following roles to an existing account:" msgstr "" "Mit diesem Link kannst du die folgenden Rollen zu einem existierenden " "Account hinzufügen:" -#: uffd/invite/templates/invite/use.html:22 +#: uffd/invite/templates/invite/use.html:16 msgid "With this link you can register a new user account." msgstr "Mit diesem Link kannst du einen Account registieren." -#: uffd/invite/templates/invite/use.html:34 +#: uffd/invite/templates/invite/use.html:28 msgid "Add the roles to your account now" msgstr "Rollen jetzt zu deinem Account hinzufügen" -#: uffd/invite/templates/invite/use.html:36 +#: uffd/invite/templates/invite/use.html:30 msgid "Logout and switch to a different account" msgstr "Abmelden und zu einem anderen Account wechseln" -#: uffd/invite/templates/invite/use.html:39 +#: uffd/invite/templates/invite/use.html:33 msgid "Logout to register a new account" msgstr "Abmelden um einen neuen Account zu registrieren" -#: uffd/invite/templates/invite/use.html:43 +#: uffd/invite/templates/invite/use.html:37 msgid "Register a new account" msgstr "Neuen Account registrieren" -#: uffd/invite/templates/invite/use.html:46 +#: uffd/invite/templates/invite/use.html:40 msgid "Login and add the roles to your account" msgstr "Anmelden und die Rollen zu deinem Account hinzufügen" @@ -372,30 +373,32 @@ msgstr "Eine Adresse pro Zeile" #: uffd/mail/templates/mail/show.html:27 uffd/role/templates/role/show.html:13 #: uffd/rolemod/templates/rolemod/show.html:10 -#: uffd/user/templates/user/show.html:7 +#: uffd/user/templates/group/show.html:8 uffd/user/templates/user/show.html:7 msgid "Save" msgstr "Speichern" #: uffd/mail/templates/mail/show.html:30 uffd/mail/templates/mail/show.html:32 #: uffd/mfa/templates/mfa/setup.html:117 uffd/mfa/templates/mfa/setup.html:179 #: uffd/role/templates/role/show.html:21 uffd/role/templates/role/show.html:24 -#: uffd/user/templates/user/show.html:11 uffd/user/templates/user/show.html:13 +#: uffd/user/templates/group/show.html:11 +#: uffd/user/templates/group/show.html:13 uffd/user/templates/user/show.html:11 +#: uffd/user/templates/user/show.html:13 msgid "Delete" msgstr "Löschen" -#: uffd/mfa/views.py:53 +#: uffd/mfa/views.py:51 msgid "Two-factor authentication was reset" msgstr "Zwei-Faktor-Authentifizierung wurde zurückgesetzt" -#: uffd/mfa/views.py:82 +#: uffd/mfa/views.py:80 msgid "Generate recovery codes first!" msgstr "Generiere zuerst die Wiederherstellungscodes!" -#: uffd/mfa/views.py:91 +#: uffd/mfa/views.py:88 msgid "Code is invalid" msgstr "Wiederherstellungscode ist ungültig" -#: uffd/mfa/views.py:115 +#: uffd/mfa/views.py:123 #, python-format msgid "" "2FA WebAuthn support disabled because import of the fido2 module failed " @@ -404,16 +407,16 @@ msgstr "" "2FA WebAuthn Unterstützung deaktiviert, da das fido2 Modul nicht geladen " "werden konnte (%s)" -#: uffd/mfa/views.py:224 +#: uffd/mfa/views.py:232 #, python-format msgid "We received too many invalid attempts! Please wait at least %s." msgstr "Wir haben zu viele fehlgeschlagene Versuche! Bitte warte mindestens %s." -#: uffd/mfa/views.py:238 +#: uffd/mfa/views.py:246 msgid "You have exhausted your recovery codes. Please generate new ones now!" msgstr "Du hast keine Wiederherstellungscode mehr. Bitte generiere diese jetzt!" -#: uffd/mfa/views.py:241 +#: uffd/mfa/views.py:249 msgid "" "You only have a few recovery codes remaining. Make sure to generate new " "ones before they run out." @@ -421,38 +424,38 @@ msgstr "" "Du hast nur noch wenige Wiederherstellungscodes übrig. Bitte generiere " "diese erneut bevor keine mehr übrig sind." -#: uffd/mfa/views.py:245 +#: uffd/mfa/views.py:253 msgid "Two-factor authentication failed" msgstr "Zwei-Faktor-Authentifizierung fehlgeschlagen" -#: uffd/mfa/templates/mfa/auth.html:12 +#: uffd/mfa/templates/mfa/auth.html:6 #: uffd/selfservice/templates/selfservice/self.html:67 msgid "Two-Factor Authentication" msgstr "Zwei-Faktor-Authentifizierung" -#: uffd/mfa/templates/mfa/auth.html:17 +#: uffd/mfa/templates/mfa/auth.html:11 msgid "Enable javascript for authentication with U2F/FIDO2 devices" msgstr "Aktiviere Javascript zur Authentifizierung mit U2F/FIDO2 Geräten" -#: uffd/mfa/templates/mfa/auth.html:21 +#: uffd/mfa/templates/mfa/auth.html:15 msgid "Authentication with U2F/FIDO2 devices is not supported by your browser" msgstr "" "Authentifizierung mit U2F/FIDO2 Geräten wird von deinem Browser nicht " "unterstützt" -#: uffd/mfa/templates/mfa/auth.html:27 +#: uffd/mfa/templates/mfa/auth.html:21 msgid "Authenticate with U2F/FIDO2 device" msgstr "Authentifiziere dich mit einem U2F/FIDO2 Gerät" -#: uffd/mfa/templates/mfa/auth.html:30 +#: uffd/mfa/templates/mfa/auth.html:24 msgid "or" msgstr "oder" -#: uffd/mfa/templates/mfa/auth.html:33 +#: uffd/mfa/templates/mfa/auth.html:27 msgid "Code from your authenticator app or recovery code" msgstr "Code aus deiner Authentifikator-App oder Wiederherstellungscode" -#: uffd/mfa/templates/mfa/auth.html:36 +#: uffd/mfa/templates/mfa/auth.html:30 msgid "Verify" msgstr "Verifizieren" @@ -700,12 +703,12 @@ msgstr "Sekunden" msgid "Verify and complete setup" msgstr "Verifiziere und beende das Setup" -#: uffd/oauth2/models.py:30 +#: uffd/oauth2/models.py:29 msgid "You need to login to access this service" msgstr "Du musst dich anmelden, um auf diesen Dienst zugreifen zu können" -#: uffd/oauth2/views.py:140 uffd/selfservice/views.py:79 -#: uffd/session/views.py:97 +#: uffd/oauth2/views.py:173 uffd/selfservice/views.py:72 +#: uffd/session/views.py:73 #, python-format msgid "" "We received too many requests from your ip address/network! Please wait " @@ -714,15 +717,15 @@ msgstr "" "Wir haben zu viele Anfragen von deiner IP-Adresses bzw. aus deinem " "Netzwerk empfangen! Bitte warte mindestens %(delay)s." -#: uffd/oauth2/views.py:148 +#: uffd/oauth2/views.py:181 msgid "Device login is currently not available. Try again later!" msgstr "Geräte-Login ist gerade nicht verfügbar. Versuche es später nochmal!" -#: uffd/oauth2/views.py:161 +#: uffd/oauth2/views.py:194 msgid "Device login failed" msgstr "Gerätelogin fehlgeschlagen" -#: uffd/oauth2/views.py:174 +#: uffd/oauth2/views.py:207 #, python-format msgid "" "You don't have the permission to access the service " @@ -731,15 +734,15 @@ msgstr "" "Du bist nicht berechtigt, auf den Dienst <b>%(service_name)s</b> " "zuzugreifen." -#: uffd/oauth2/templates/oauth2/logout.html:10 uffd/templates/base.html:99 +#: uffd/oauth2/templates/oauth2/logout.html:5 uffd/templates/base.html:99 msgid "Logout" msgstr "Abmelden" -#: uffd/oauth2/templates/oauth2/logout.html:15 +#: uffd/oauth2/templates/oauth2/logout.html:10 msgid "Javascript is required for automatic logout" msgstr "Für das automatische Abmelden muss Javascript aktiviert sein" -#: uffd/oauth2/templates/oauth2/logout.html:17 +#: uffd/oauth2/templates/oauth2/logout.html:12 msgid "" "While you successfully logged out of the Single-Sign-On service, you may " "still be logged in on these services:" @@ -747,7 +750,7 @@ msgstr "" "Während du nun aus dem Single-Sign-On abgemeldet bist, bist du eventuell " "weiterhin in folgenden Diensten angemeldet:" -#: uffd/oauth2/templates/oauth2/logout.html:30 +#: uffd/oauth2/templates/oauth2/logout.html:25 msgid "" "Please wait until you have been automatically logged out of all services " "or make sure of this yourself." @@ -755,31 +758,31 @@ msgstr "" "Bitte warte, bis das automatische Abmelden bei allen Diensten " "abgeschlossen ist oder melde dich überall manuell ab." -#: uffd/oauth2/templates/oauth2/logout.html:34 +#: uffd/oauth2/templates/oauth2/logout.html:29 msgid "Logging you out on all services ..." msgstr "Abmeldung bei allen Diensten ..." -#: uffd/oauth2/templates/oauth2/logout.html:38 +#: uffd/oauth2/templates/oauth2/logout.html:33 msgid "Skip this and continue" msgstr "Automatisches Abmelden überspringen" -#: uffd/oauth2/templates/oauth2/logout.html:82 +#: uffd/oauth2/templates/oauth2/logout.html:75 msgid "Done, redirecting ..." msgstr "Abgeschlossen, leite weiter ..." -#: uffd/oauth2/templates/oauth2/logout.html:86 +#: uffd/oauth2/templates/oauth2/logout.html:79 msgid "Log out failed on some services. Retry?" msgstr "" "Automatisches Abmelden bei einigen Diensten fehlgeschlagen. Nochmal " "versuchen?" -#: uffd/role/views.py:49 uffd/selfservice/templates/selfservice/self.html:86 +#: uffd/role/views.py:48 uffd/selfservice/templates/selfservice/self.html:86 #: uffd/user/templates/user/list.html:20 uffd/user/templates/user/show.html:21 #: uffd/user/templates/user/show.html:90 msgid "Roles" msgstr "Rollen" -#: uffd/role/views.py:99 +#: uffd/role/views.py:95 msgid "Locked roles cannot be deleted" msgstr "Gesperrte Rollen können nicht gelöscht werden" @@ -809,7 +812,7 @@ msgstr "Als Default setzen" #: uffd/role/templates/role/show.html:19 uffd/role/templates/role/show.html:21 #: uffd/selfservice/templates/selfservice/self.html:117 -#: uffd/user/templates/user/show.html:11 +#: uffd/user/templates/group/show.html:11 uffd/user/templates/user/show.html:11 msgid "Are you sure?" msgstr "Wirklich fortfahren?" @@ -847,7 +850,7 @@ msgstr "Accounts mit Moderationsrechten" #: uffd/role/templates/role/show.html:71 #: uffd/rolemod/templates/rolemod/show.html:18 -#: uffd/user/templates/group/show.html:15 +#: uffd/user/templates/group/show.html:37 msgid "Members" msgstr "Mitglieder" @@ -871,15 +874,15 @@ msgstr "derzeit enthaltene Gruppen" msgid "2FA required" msgstr "2FA erforderlich" -#: uffd/rolemod/views.py:24 +#: uffd/rolemod/views.py:23 msgid "Moderation" msgstr "Moderation" -#: uffd/rolemod/views.py:46 +#: uffd/rolemod/views.py:43 msgid "Description too long" msgstr "Beschreibung zu lang" -#: uffd/rolemod/views.py:63 +#: uffd/rolemod/views.py:60 msgid "Member removed" msgstr "Mitglied entfernt" @@ -911,27 +914,27 @@ msgstr "Entfernen" msgid "Selfservice" msgstr "Selfservice" -#: uffd/selfservice/views.py:37 +#: uffd/selfservice/views.py:36 msgid "Display name changed." msgstr "Anzeigename geändert." -#: uffd/selfservice/views.py:39 +#: uffd/selfservice/views.py:38 msgid "Display name is not valid." msgstr "Anzeigename ist nicht valide." -#: uffd/selfservice/views.py:42 +#: uffd/selfservice/views.py:41 msgid "We sent you an email, please verify your mail address." msgstr "Wir haben dir eine E-Mail gesendet, bitte prüfe deine E-Mail-Adresse." -#: uffd/selfservice/views.py:56 +#: uffd/selfservice/views.py:53 msgid "Password changed" msgstr "Passwort geändert" -#: uffd/selfservice/views.py:59 +#: uffd/selfservice/views.py:55 msgid "Invalid password" msgstr "Passwort ungültig" -#: uffd/selfservice/views.py:77 +#: uffd/selfservice/views.py:70 #, python-format msgid "" "We received too many password reset requests for this user! Please wait " @@ -940,7 +943,7 @@ msgstr "" "Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account! " "Bitte warte mindestens %(delay)s." -#: uffd/selfservice/views.py:83 +#: uffd/selfservice/views.py:76 msgid "" "We sent a mail to this user's mail address if you entered the correct " "mail and login name combination" @@ -948,27 +951,28 @@ msgstr "" "Falls E-Mail-Adresse und Anmeldename richtig waren, wurde eine E-Mail an " "die Adresse gesendet." -#: uffd/selfservice/views.py:93 uffd/selfservice/views.py:123 +#: uffd/selfservice/views.py:91 uffd/selfservice/views.py:100 +#: uffd/selfservice/views.py:133 uffd/selfservice/views.py:143 msgid "Token expired, please try again." msgstr "Link abgelaufen, bitte versuche es erneut." -#: uffd/selfservice/views.py:101 +#: uffd/selfservice/views.py:108 msgid "You need to set a password, please try again." msgstr "Password fehlt, bitte versuche es erneut." -#: uffd/selfservice/views.py:104 +#: uffd/selfservice/views.py:111 msgid "Passwords do not match, please try again." msgstr "Die Passwörter stimmen nicht überein, bitte versuche es erneut" -#: uffd/selfservice/views.py:110 +#: uffd/selfservice/views.py:117 msgid "Password ist not valid, please try again." msgstr "Ungültiges Passwort, bitte versuche es erneut" -#: uffd/selfservice/views.py:113 +#: uffd/selfservice/views.py:120 msgid "New password set" msgstr "Passwort geändert" -#: uffd/selfservice/views.py:131 +#: uffd/selfservice/views.py:150 msgid "" "This link was generated for another user. Login as the correct user to " "continue." @@ -976,41 +980,41 @@ msgstr "" "Dieser Link wurde für einen anderen Account erstellt. Melde dich mit dem " "richtigen Account an um Fortzufahren." -#: uffd/selfservice/views.py:133 +#: uffd/selfservice/views.py:152 msgid "New mail set" msgstr "E-Mail-Adresse geändert" -#: uffd/selfservice/views.py:144 +#: uffd/selfservice/views.py:162 msgid "Leaving roles is disabled" msgstr "Verlassen von Rollen ist deaktiviert" -#: uffd/selfservice/views.py:151 +#: uffd/selfservice/views.py:168 #, python-format msgid "You left role %(role_name)s" msgstr "Rolle %(role_name)s verlassen" -#: uffd/selfservice/views.py:168 uffd/selfservice/views.py:188 +#: uffd/selfservice/views.py:185 uffd/selfservice/views.py:205 #, python-format msgid "Mail to \"%(mail_address)s\" could not be sent!" msgstr "E-Mail an \"%(mail_address)s\" konnte nicht gesendet werden!" -#: uffd/selfservice/templates/selfservice/forgot_password.html:11 +#: uffd/selfservice/templates/selfservice/forgot_password.html:6 msgid "Forgot password" msgstr "Passwort vergessen" -#: uffd/selfservice/templates/selfservice/forgot_password.html:14 +#: uffd/selfservice/templates/selfservice/forgot_password.html:9 #: uffd/selfservice/templates/selfservice/self.html:21 -#: uffd/session/templates/session/login.html:14 -#: uffd/signup/templates/signup/start.html:19 +#: uffd/session/templates/session/login.html:9 +#: uffd/signup/templates/signup/start.html:14 #: uffd/user/templates/user/list.html:18 uffd/user/templates/user/show.html:48 msgid "Login Name" msgstr "Anmeldename" -#: uffd/selfservice/templates/selfservice/forgot_password.html:18 +#: uffd/selfservice/templates/selfservice/forgot_password.html:13 msgid "Mail Address" msgstr "E-Mail-Adresse" -#: uffd/selfservice/templates/selfservice/forgot_password.html:22 +#: uffd/selfservice/templates/selfservice/forgot_password.html:17 msgid "Send password reset mail" msgstr "Passwort-Zurücksetzen-Mail versenden" @@ -1043,13 +1047,13 @@ msgid "Changes may take serveral minutes to be visible in all services." msgstr "Änderungen sind erst nach einigen Minuten in allen Diensten sichtbar." #: uffd/selfservice/templates/selfservice/self.html:25 -#: uffd/signup/templates/signup/start.html:32 +#: uffd/signup/templates/signup/start.html:27 #: uffd/user/templates/user/list.html:19 uffd/user/templates/user/show.html:63 msgid "Display Name" msgstr "Anzeigename" #: uffd/selfservice/templates/selfservice/self.html:29 -#: uffd/signup/templates/signup/start.html:39 +#: uffd/signup/templates/signup/start.html:34 msgid "E-Mail Address" msgstr "E-Mail-Adresse" @@ -1064,8 +1068,8 @@ msgid "Update Profile" msgstr "Änderungen speichern" #: uffd/selfservice/templates/selfservice/self.html:44 -#: uffd/session/templates/session/login.html:18 -#: uffd/signup/templates/signup/start.html:46 +#: uffd/session/templates/session/login.html:13 +#: uffd/signup/templates/signup/start.html:41 #: uffd/user/templates/user/show.html:77 msgid "Password" msgstr "Passwort" @@ -1082,13 +1086,13 @@ msgstr "" " Support-Anfragen benötigt." #: uffd/selfservice/templates/selfservice/self.html:50 -#: uffd/selfservice/templates/selfservice/set_password.html:14 +#: uffd/selfservice/templates/selfservice/set_password.html:9 msgid "New Password" msgstr "Neues Passwort" #: uffd/selfservice/templates/selfservice/self.html:56 -#: uffd/selfservice/templates/selfservice/set_password.html:21 -#: uffd/signup/templates/signup/start.html:53 +#: uffd/selfservice/templates/selfservice/set_password.html:16 +#: uffd/signup/templates/signup/start.html:48 msgid "Repeat Password" msgstr "Passwort wiederholen" @@ -1153,11 +1157,11 @@ msgstr "Verlassen" msgid "You currently don't have any roles" msgstr "Du hast derzeit keine Rollen" -#: uffd/selfservice/templates/selfservice/set_password.html:11 +#: uffd/selfservice/templates/selfservice/set_password.html:6 msgid "Reset password" msgstr "Passwort zurücksetzen" -#: uffd/selfservice/templates/selfservice/set_password.html:25 +#: uffd/selfservice/templates/selfservice/set_password.html:20 msgid "Set password" msgstr "Passwort setzen" @@ -1182,7 +1186,7 @@ msgstr "Kein Zugriff" msgid "Close" msgstr "Schließen" -#: uffd/session/views.py:95 +#: uffd/session/views.py:71 #, python-format msgid "" "We received too many invalid login attempts for this user! Please wait at" @@ -1191,52 +1195,52 @@ msgstr "" "Wir haben zu viele fehlgeschlagene Anmeldeversuche für diesen Account " "erhalten! Bitte warte mindestens %(delay)s." -#: uffd/session/views.py:103 +#: uffd/session/views.py:79 msgid "Login name or password is wrong" msgstr "Der Anmeldename oder das Passwort ist falsch" -#: uffd/session/views.py:106 +#: uffd/session/views.py:82 msgid "You do not have access to this service" msgstr "Du hast keinen Zugriff auf diesen Service" -#: uffd/session/views.py:118 uffd/session/views.py:129 +#: uffd/session/views.py:94 uffd/session/views.py:105 msgid "You need to login first" msgstr "Du musst dich erst anmelden" -#: uffd/session/views.py:150 uffd/session/views.py:160 +#: uffd/session/views.py:126 uffd/session/views.py:136 msgid "Initiation code is no longer valid" msgstr "Startcode ist nicht mehr gültig" -#: uffd/session/views.py:164 +#: uffd/session/views.py:140 msgid "Invalid confirmation code" msgstr "Ungültiger Bestätigungscode" -#: uffd/session/views.py:176 uffd/session/views.py:187 +#: uffd/session/views.py:152 uffd/session/views.py:163 msgid "Invalid initiation code" msgstr "Ungültiger Startcode" -#: uffd/session/templates/session/deviceauth.html:17 +#: uffd/session/templates/session/deviceauth.html:12 #: uffd/templates/base.html:92 msgid "Authorize Device Login" msgstr "Gerätelogin erlauben" -#: uffd/session/templates/session/deviceauth.html:20 +#: uffd/session/templates/session/deviceauth.html:15 msgid "Log into a service on another device without entering your password." msgstr "" "Melde dich an einem Dienst auf einem anderen Gerät an ohne dein Password " "eingeben zu müssen." -#: uffd/session/templates/session/deviceauth.html:23 -#: uffd/session/templates/session/devicelogin.html:18 +#: uffd/session/templates/session/deviceauth.html:18 +#: uffd/session/templates/session/devicelogin.html:13 msgid "Initiation Code" msgstr "Startcode" -#: uffd/session/templates/session/deviceauth.html:32 -#: uffd/session/templates/session/devicelogin.html:23 +#: uffd/session/templates/session/deviceauth.html:27 +#: uffd/session/templates/session/devicelogin.html:18 msgid "Confirmation Code" msgstr "Bestätigungscode" -#: uffd/session/templates/session/deviceauth.html:38 +#: uffd/session/templates/session/deviceauth.html:33 msgid "" "Start logging into a service on the other device and chose \"Device " "Login\" on the login page. Enter the displayed initiation code in the box" @@ -1246,21 +1250,21 @@ msgstr "" "\"Gerätelogin\" auf der Anmeldeseite aus. Gib den angezeigten Startcode " "oben ein." -#: uffd/session/templates/session/deviceauth.html:41 -#: uffd/session/templates/session/devicelogin.html:30 +#: uffd/session/templates/session/deviceauth.html:36 +#: uffd/session/templates/session/devicelogin.html:25 msgid "Continue" msgstr "Weiter" -#: uffd/session/templates/session/deviceauth.html:48 +#: uffd/session/templates/session/deviceauth.html:43 #, python-format msgid "Authorize the login for service <b>%(service_name)s</b>?" msgstr "Anmeldung an Dienst <b>%(service_name)s</b> erlauben?" -#: uffd/session/templates/session/deviceauth.html:51 +#: uffd/session/templates/session/deviceauth.html:46 msgid "Authorize Login" msgstr "Anmeldung erlauben" -#: uffd/session/templates/session/deviceauth.html:58 +#: uffd/session/templates/session/deviceauth.html:53 msgid "" "Enter the confirmation code on the other device and complete the login. " "Click <em>Finish</em> afterwards." @@ -1268,16 +1272,16 @@ msgstr "" "Gib den Bestätigungscode auf dem anderen Gerät ein und schließe die " "Anmeldung ab. Clicke danach auf <em>Abschließen</em>." -#: uffd/session/templates/session/deviceauth.html:61 +#: uffd/session/templates/session/deviceauth.html:56 msgid "Finish" msgstr "Beenden" -#: uffd/session/templates/session/devicelogin.html:11 +#: uffd/session/templates/session/devicelogin.html:6 #: uffd/templates/base.html:93 msgid "Device Login" msgstr "Gerätelogin" -#: uffd/session/templates/session/devicelogin.html:14 +#: uffd/session/templates/session/devicelogin.html:9 msgid "" "Use a login session on another device (e.g. your laptop) to log into a " "service without entering your password." @@ -1285,7 +1289,7 @@ msgstr "" "Nutze eine Login-Sitzung auf einem anderen Gerät (z.B. deinem Laptop) um " "dich bei einem Dienst anzumelden." -#: uffd/session/templates/session/devicelogin.html:27 +#: uffd/session/templates/session/devicelogin.html:22 #, python-format msgid "" "Open <code><a href=\"%(deviceauth_url)s\">%(deviceauth_url)s</a></code> " @@ -1296,65 +1300,65 @@ msgstr "" "auf dem anderen Gerät und gib dort den obenstehenden Startcode ein. Geben" " anschließend den Bestätigungscode hier ein." -#: uffd/session/templates/session/login.html:11 -#: uffd/session/templates/session/login.html:22 +#: uffd/session/templates/session/login.html:6 +#: uffd/session/templates/session/login.html:17 msgid "Login" msgstr "Anmelden" -#: uffd/session/templates/session/login.html:25 +#: uffd/session/templates/session/login.html:20 msgid "- or -" msgstr "- oder -" -#: uffd/session/templates/session/login.html:27 +#: uffd/session/templates/session/login.html:22 msgid "Login with another device" msgstr "Über anderes Gerät anmelden" -#: uffd/session/templates/session/login.html:32 +#: uffd/session/templates/session/login.html:27 msgid "Register" msgstr "Registrieren" -#: uffd/session/templates/session/login.html:35 +#: uffd/session/templates/session/login.html:30 msgid "Forgot Password?" msgstr "Passwort vergessen?" -#: uffd/signup/views.py:23 +#: uffd/signup/views.py:24 msgid "Singup not enabled" msgstr "Account-Registrierung ist deaktiviert" -#: uffd/signup/views.py:77 uffd/signup/views.py:85 +#: uffd/signup/views.py:82 uffd/signup/views.py:91 uffd/signup/views.py:99 msgid "Invalid signup link" msgstr "Ungültiger Account-Registrierungs-Link" -#: uffd/signup/views.py:90 +#: uffd/signup/views.py:104 #, python-format msgid "Too many failed attempts! Please wait %(delay)s." msgstr "Zu viele fehlgeschlagene Versuche! Bitte warte mindestens %(delay)s." -#: uffd/signup/views.py:96 +#: uffd/signup/views.py:110 msgid "Wrong password" msgstr "Falsches Passwort" -#: uffd/signup/views.py:103 +#: uffd/signup/views.py:116 msgid "Your account was successfully created" msgstr "Account erfolgreich erstellt" -#: uffd/signup/templates/signup/confirm.html:11 +#: uffd/signup/templates/signup/confirm.html:6 msgid "Complete Registration" msgstr "Account-Registrierung abschließen" -#: uffd/signup/templates/signup/confirm.html:17 +#: uffd/signup/templates/signup/confirm.html:12 msgid "Please enter your password to complete the account registration" msgstr "Bitte gib dein Passwort ein, um die Account-Registrierung abzuschließen" -#: uffd/signup/templates/signup/confirm.html:21 +#: uffd/signup/templates/signup/confirm.html:16 msgid "Complete Account Registration" msgstr "Account-Registrierung abschließen" -#: uffd/signup/templates/signup/start.html:23 +#: uffd/signup/templates/signup/start.html:18 msgid "Check" msgstr "Überprüfen" -#: uffd/signup/templates/signup/start.html:28 +#: uffd/signup/templates/signup/start.html:23 msgid "" "At least one and at most 32 lower-case characters, digits, dashes (\"-\")" " or underscores (\"_\"). <b>Cannot be changed later!</b>" @@ -1362,11 +1366,11 @@ msgstr "" "1 bis 32 Kleinbuchstaben, Zahlen, Binde- (\"-\") und Unterstriche " "(\"_\"). <b>Kann später nicht geändert werden!</b>" -#: uffd/signup/templates/signup/start.html:35 +#: uffd/signup/templates/signup/start.html:30 msgid "At least one and at most 128 characters, no other special requirements." msgstr "Mindestens 1 und maximal 128 Zeichen, keine weiteren Einschränkungen." -#: uffd/signup/templates/signup/start.html:42 +#: uffd/signup/templates/signup/start.html:37 msgid "" "We will send a confirmation mail to this address that you need to " "complete the registration." @@ -1374,27 +1378,27 @@ msgstr "" "Wir werden eine Bestätigungsmail an diese Adresse senden. Du benötigst " "sie, um die Account-Registrierung abzuschließen." -#: uffd/signup/templates/signup/start.html:57 +#: uffd/signup/templates/signup/start.html:52 msgid "Create Account" msgstr "Account registrieren" -#: uffd/signup/templates/signup/start.html:86 +#: uffd/signup/templates/signup/start.html:79 msgid "The name is already taken" msgstr "Dieser Name wird bereits verwendet" -#: uffd/signup/templates/signup/start.html:89 +#: uffd/signup/templates/signup/start.html:82 msgid "Too many requests! Please wait a bit before trying again!" msgstr "Zu viele Anfragen! Bitte warte etwas, bevor du es erneut versuchst!" -#: uffd/signup/templates/signup/start.html:92 +#: uffd/signup/templates/signup/start.html:85 msgid "The name is invalid" msgstr "Name ungültig" -#: uffd/signup/templates/signup/submitted.html:11 +#: uffd/signup/templates/signup/submitted.html:5 msgid "Confirm your E-Mail Address" msgstr "E-Mail-Adresse bestätigen" -#: uffd/signup/templates/signup/submitted.html:13 +#: uffd/signup/templates/signup/submitted.html:7 #, python-format msgid "" "We sent a confirmation mail to <b>%(signup_mail)s</b>. You need to " @@ -1405,7 +1409,7 @@ msgstr "" "deine E-Mail-Adresse innerhalb von 48 Stunden bestätigen, um die Account-" "Registrierung abzuschließen." -#: uffd/signup/templates/signup/submitted.html:14 +#: uffd/signup/templates/signup/submitted.html:8 msgid "" "If you mistyped your mail address or don't receive the confirmation mail " "for another reason, retry the registration procedure from the beginning." @@ -1430,7 +1434,7 @@ msgstr "Ändern" msgid "About uffd" msgstr "Über uffd" -#: uffd/user/models.py:52 +#: uffd/user/models.py:77 #, python-format msgid "" "At least %(minlen)d and at most %(maxlen)d characters. Only letters, " @@ -1441,10 +1445,26 @@ msgstr "" "und manche Symbole (<code>%(symbols)s</code>), keine Umlaute. Bitte " "verwende einen Passwort-Manager." -#: uffd/user/views_group.py:20 +#: uffd/user/views_group.py:23 msgid "Groups" msgstr "Gruppen" +#: uffd/user/views_group.py:51 +msgid "Group with this name or id already exists" +msgstr "Gruppe mit diesem Namen oder dieser ID existiert bereits" + +#: uffd/user/views_group.py:56 +msgid "Group created" +msgstr "Gruppe erstellt" + +#: uffd/user/views_group.py:58 +msgid "Group updated" +msgstr "Gruppe aktualisiert" + +#: uffd/user/views_group.py:67 +msgid "Deleted group" +msgstr "Gruppe gelöscht" + #: uffd/user/views_user.py:30 msgid "Users" msgstr "Accounts" @@ -1465,28 +1485,32 @@ msgstr "Anzeigename entspricht nicht den Anforderungen" msgid "Password is invalid" msgstr "Passwort ist ungültig" -#: uffd/user/views_user.py:78 +#: uffd/user/views_user.py:77 msgid "Service user created" msgstr "Service-Account erstellt" -#: uffd/user/views_user.py:81 +#: uffd/user/views_user.py:80 msgid "User created. We sent the user a password reset link by mail" msgstr "" "Account erstellt. E-Mail mit einem Link zum Setzen des Passworts wurde " "versendet." -#: uffd/user/views_user.py:83 +#: uffd/user/views_user.py:82 msgid "User updated" msgstr "Account aktualisiert" -#: uffd/user/views_user.py:94 +#: uffd/user/views_user.py:92 msgid "Deleted user" msgstr "Account gelöscht" -#: uffd/user/templates/group/list.html:9 uffd/user/templates/group/show.html:7 +#: uffd/user/templates/group/list.html:14 msgid "GID" msgstr "GID" +#: uffd/user/templates/group/show.html:18 +msgid "Group ID" +msgstr "Gruppen ID" + #: uffd/user/templates/user/list.html:11 msgid "CSV import" msgstr "CSV-Import" diff --git a/uffd/user/models.py b/uffd/user/models.py index 3a3069f7..ea54d7c2 100644 --- a/uffd/user/models.py +++ b/uffd/user/models.py @@ -1,42 +1,67 @@ import secrets import string import re +import hashlib +import base64 from flask import current_app, escape from flask_babel import lazy_gettext -from ldap3.utils.hashed import hashed, HASHED_SALTED_SHA512 +from sqlalchemy import Column, Integer, String, ForeignKey, Boolean +from sqlalchemy.orm import relationship +from sqlalchemy.sql.expression import func + +from uffd.database import db + +# Interface inspired by argon2-cffi +class PasswordHasher: + # pylint: disable=no-self-use + def hash(self, password): + salt = secrets.token_bytes(8) + ctx = hashlib.sha512() + ctx.update(password.encode()) + ctx.update(salt) + return '{ssha512}'+base64.b64encode(ctx.digest()+salt).decode() + + def verify(self, hash, password): + if hash is None: + return False + if hash.startswith('{ssha512}'): + data = base64.b64decode(hash[len('{ssha512}'):].encode()) + ctx = hashlib.sha512() + digest = data[:ctx.digest_size] + salt = data[ctx.digest_size:] + ctx.update(password.encode()) + ctx.update(salt) + return secrets.compare_digest(digest, ctx.digest()) + return False -from uffd.ldap import ldap -from uffd.lazyconfig import lazyconfig_str, lazyconfig_list + # pylint: disable=unused-argument + def check_needs_rehash(self, hash): + return False -def get_next_uid(service=False): - if service: - new_uid_min = current_app.config['LDAP_USER_SERVICE_MIN_UID'] - new_uid_max = current_app.config['LDAP_USER_SERVICE_MAX_UID'] +def get_next_unix_uid(context): + is_service_user = bool(context.get_current_parameters().get('is_service_user', False)) + if is_service_user: + min_uid = current_app.config['USER_SERVICE_MIN_UID'] + max_uid = current_app.config['USER_SERVICE_MAX_UID'] else: - new_uid_min = current_app.config['LDAP_USER_MIN_UID'] - new_uid_max = current_app.config['LDAP_USER_MAX_UID'] - next_uid = new_uid_min - for user in User.query.all(): - if user.uid <= new_uid_max: - next_uid = max(next_uid, user.uid + 1) - if next_uid > new_uid_max: + min_uid = current_app.config['USER_MIN_UID'] + max_uid = current_app.config['USER_MAX_UID'] + next_uid = max(min_uid, + db.session.query(func.max(User.unix_uid + 1))\ + .filter(User.is_service_user==is_service_user)\ + .scalar() or 0) + if next_uid > max_uid: raise Exception('No free uid found') return next_uid -class ObjectAttributeDict: - def __init__(self, obj): - self.obj = obj - - def __getitem__(self, key): - return getattr(self.obj, key) +# pylint: disable=E1101 +user_groups = db.Table('user_groups', + Column('user_id', Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), + Column('group_id', Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) +) -def format_with_attributes(fmtstr, obj): - # Do str.format-style string formatting with the attributes of an object - # E.g. format_with_attributes("/home/{loginname}", obj) = "/home/foobar" if obj.loginname = "foobar" - return fmtstr.format_map(ObjectAttributeDict(obj)) - -class BaseUser(ldap.Model): +class User(db.Model): # Allows 8 to 256 ASCII letters (lower and upper case), digits, spaces and # symbols/punctuation characters. It disallows control characters and # non-ASCII characters to prevent setting passwords considered invalid by @@ -51,57 +76,29 @@ class BaseUser(ldap.Model): 'Please use a password manager.', minlen=PASSWORD_MINLEN, maxlen=PASSWORD_MAXLEN, symbols=escape('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~')) - ldap_search_base = lazyconfig_str('LDAP_USER_SEARCH_BASE') - ldap_filter_params = lazyconfig_list('LDAP_USER_SEARCH_FILTER') - ldap_object_classes = lazyconfig_list('LDAP_USER_OBJECTCLASSES') - ldap_dn_base = lazyconfig_str('LDAP_USER_SEARCH_BASE') - ldap_dn_attribute = lazyconfig_str('LDAP_USER_DN_ATTRIBUTE') - - uid = ldap.Attribute(lazyconfig_str('LDAP_USER_UID_ATTRIBUTE'), default=get_next_uid, aliases=lazyconfig_list('LDAP_USER_UID_ALIASES')) - loginname = ldap.Attribute(lazyconfig_str('LDAP_USER_LOGINNAME_ATTRIBUTE'), aliases=lazyconfig_list('LDAP_USER_LOGINNAME_ALIASES')) - displayname = ldap.Attribute(lazyconfig_str('LDAP_USER_DISPLAYNAME_ATTRIBUTE'), aliases=lazyconfig_list('LDAP_USER_DISPLAYNAME_ALIASES')) - mail = ldap.Attribute(lazyconfig_str('LDAP_USER_MAIL_ATTRIBUTE'), aliases=lazyconfig_list('LDAP_USER_MAIL_ALIASES')) - pwhash = ldap.Attribute('userPassword', default=lambda: hashed(HASHED_SALTED_SHA512, secrets.token_hex(128))) - - groups = set() # Shuts up pylint, overwritten by back-reference - roles = set() # Shuts up pylint, overwritten by back-reference + __tablename__ = 'user' + id = Column(Integer(), primary_key=True, autoincrement=True) + unix_uid = Column(Integer(), unique=True, nullable=False, default=get_next_unix_uid) + loginname = Column(String(32), unique=True, nullable=False) + displayname = Column(String(128), nullable=False) + mail = Column(String(128), nullable=False) + pwhash = Column(String(256), nullable=True) + is_service_user = Column(Boolean(), default=False, nullable=False) + groups = relationship('Group', secondary='user_groups') + roles = relationship('Role', secondary='role_members', back_populates='members') @property - def group_dns(self): - return [group.dn for group in self.groups] - - @property - def is_service_user(self): - if self.uid is None: - return None - return self.uid >= current_app.config['LDAP_USER_SERVICE_MIN_UID'] and self.uid <= current_app.config['LDAP_USER_SERVICE_MAX_UID'] - - @is_service_user.setter - def is_service_user(self, value): - assert self.uid is None - if value: - self.uid = get_next_uid(service=True) - - def add_default_attributes(self): - for name, values in current_app.config['LDAP_USER_DEFAULT_ATTRIBUTES'].items(): - if self.ldap_object.getattr(name): - continue - if not isinstance(values, list): - values = [values] - formatted_values = [] - for value in values: - if isinstance(value, str): - value = format_with_attributes(value, self) - formatted_values.append(value) - self.ldap_object.setattr(name, formatted_values) - - ldap_add_hooks = ldap.Model.ldap_add_hooks + (add_default_attributes,) + def unix_gid(self): + return current_app.config['USER_GID'] # Write-only property def password(self, value): - self.pwhash = hashed(HASHED_SALTED_SHA512, value) + self.pwhash = PasswordHasher().hash(value) password = property(fset=password) + def check_password(self, value): + return PasswordHasher().verify(self.pwhash, value) + def is_in_group(self, name): if not name: return True @@ -155,15 +152,17 @@ class BaseUser(ldap.Model): self.mail = value return True -User = BaseUser - -class Group(ldap.Model): - ldap_search_base = lazyconfig_str('LDAP_GROUP_SEARCH_BASE') - ldap_filter_params = lazyconfig_list('LDAP_GROUP_SEARCH_FILTER') - - gid = ldap.Attribute(lazyconfig_str('LDAP_GROUP_GID_ATTRIBUTE')) - name = ldap.Attribute(lazyconfig_str('LDAP_GROUP_NAME_ATTRIBUTE')) - description = ldap.Attribute(lazyconfig_str('LDAP_GROUP_DESCRIPTION_ATTRIBUTE'), default='') - members = ldap.Relationship(lazyconfig_str('LDAP_GROUP_MEMBER_ATTRIBUTE'), User, backref='groups') - - roles = [] # Shuts up pylint, overwritten by back-reference +def get_next_unix_gid(): + next_gid = max(current_app.config['GROUP_MIN_GID'], + db.session.query(func.max(Group.unix_gid + 1)).scalar() or 0) + if next_gid > current_app.config['GROUP_MAX_GID']: + raise Exception('No free gid found') + return next_gid + +class Group(db.Model): + __tablename__ = 'group' + id = Column(Integer(), primary_key=True, autoincrement=True) + unix_gid = Column(Integer(), unique=True, nullable=False, default=get_next_unix_gid) + name = Column(String(32), unique=True, nullable=False) + description = Column(String(128), nullable=False, default='') + members = relationship('User', secondary='user_groups') diff --git a/uffd/user/templates/group/list.html b/uffd/user/templates/group/list.html index e393485d..fbad73b6 100644 --- a/uffd/user/templates/group/list.html +++ b/uffd/user/templates/group/list.html @@ -3,6 +3,11 @@ {% block body %} <div class="row"> <div class="col"> + <p class="text-right"> + <a class="btn btn-primary" href="{{ url_for("group.show") }}"> + <i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}} + </a> + </p> <table class="table table-striped table-sm"> <thead> <tr> @@ -12,13 +17,13 @@ </tr> </thead> <tbody> - {% for group in groups|sort(attribute="gid") %} - <tr id="group-{{ group.gid }}"> + {% for group in groups|sort(attribute="unix_gid") %} + <tr id="group-{{ group.id }}"> <th scope="row"> - {{ group.gid }} + {{ group.unix_gid }} </th> <td> - <a href="{{ url_for("group.show", gid=group.gid) }}"> + <a href="{{ url_for("group.show", id=group.id) }}"> {{ group.name }} </a> </td> diff --git a/uffd/user/templates/group/show.html b/uffd/user/templates/group/show.html index d43d7406..cc4f7970 100644 --- a/uffd/user/templates/group/show.html +++ b/uffd/user/templates/group/show.html @@ -1,24 +1,47 @@ {% extends 'base.html' %} {% block body %} -<form action="{{ url_for("group.show", gid=group.gid) }}" method="POST"> +<form action="{{ url_for("group.update", id=group.id) }}" method="POST"> <div class="align-self-center"> + <div class="clearfix pb-2 col"> + <div class="float-sm-right"> + <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button> + <a href="{{ url_for("group.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a> + {% if group.id %} + <a href="{{ url_for("group.delete", id=group.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a> + {% else %} + <a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a> + {% endif %} + </div> + </div> <div class="form-group col"> - <label for="group-gid">{{_("GID")}}</label> - <input type="number" class="form-control" id="group-gid" name="gid" value="{{ group.gid }}" readonly> + <label for="group-gid">{{_("Group ID")}}</label> + {% if not group.id %} + <input type="number" class="form-control" id="group-gid" name="unix_gid" value="" placeholder="Automatically chosen if empty"> + {% else %} + <input type="number" class="form-control" id="group-gid" name="unix_gid" value="{{ group.unix_gid }}" readonly> + {% endif %} </div> <div class="form-group col"> <label for="group-loginname">{{_("Name")}}</label> - <input type="text" class="form-control" id="group-loginname" name="loginname" value="{{ group.name }}" readonly> + <input type="text" class="form-control" id="group-loginname" name="name" value="{{ group.name or '' }}" {{ 'readonly' if group.id }}> + </div> + <div class="form-group col"> + <label for="group-description">{{_("Description")}}</label> + <textarea class="form-control" id="group-description" name="description" rows="5">{{ group.description or '' }}</textarea> + <small class="form-text text-muted"> + </small> </div> + {% if group.id %} <div class="col"> <span>{{_("Members")}}:</span> <ul class="row"> {% for member in group.members|sort(attribute='loginname') %} - <li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", uid=member.uid) }}">{{ member.loginname }}</a></li> + <li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=member.id) }}">{{ member.loginname }}</a></li> {% endfor %} </ul> </div> + {% endif %} </div> </form> {% endblock %} diff --git a/uffd/user/templates/user/list.html b/uffd/user/templates/user/list.html index 61f5b68d..63748d0f 100644 --- a/uffd/user/templates/user/list.html +++ b/uffd/user/templates/user/list.html @@ -21,13 +21,13 @@ </tr> </thead> <tbody> - {% for user in users|sort(attribute="uid") %} - <tr id="user-{{ user.uid }}"> + {% for user in users|sort(attribute="unix_uid") %} + <tr id="user-{{ user.id }}"> <th scope="row"> - {{ user.uid }} + {{ user.unix_uid }} </th> <td> - <a href="{{ url_for("user.show", uid=user.uid) }}"> + <a href="{{ url_for("user.show", id=user.id) }}"> {{ user.loginname }} </a> {% if user.is_service_user %} diff --git a/uffd/user/templates/user/show.html b/uffd/user/templates/user/show.html index 052dd09a..bd5a0a65 100644 --- a/uffd/user/templates/user/show.html +++ b/uffd/user/templates/user/show.html @@ -1,14 +1,14 @@ {% extends 'base.html' %} {% block body %} -<form action="{{ url_for("user.update", uid=user.uid) }}" method="POST"> +<form action="{{ url_for("user.update", id=user.id) }}" method="POST"> <div class="align-self-center"> <div class="float-sm-right pb-2"> <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button> <a href="{{ url_for("user.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a> - {% if user.uid %} - <a href="{{ url_for("mfa.admin_disable", uid=user.uid) }}" class="btn btn-secondary">{{_("Reset 2FA")}}</a> - <a href="{{ url_for("user.delete", uid=user.uid) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a> + {% if user.id %} + <a href="{{ url_for("mfa.admin_disable", id=user.id) }}" class="btn btn-secondary">{{_("Reset 2FA")}}</a> + <a href="{{ url_for("user.delete", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a> {% else %} <a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a> {% endif %} @@ -30,13 +30,13 @@ <span class="badge badge-secondary">{{_('service')}}</span> {% endif %} </label> - {% if user.uid %} - <input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid or '' }}" readonly> + {% if user.id %} + <input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.unix_uid }}" readonly> {% else %} <input type="text" class="form-control" id="user-uid" name="uid" placeholder="{{_('will be choosen')}}" readonly> {% endif %} </div> - {% if not user.uid %} + {% if not user.id %} <div class="form-group col"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="user-serviceaccount" name="serviceaccount" value="1" aria-label="enabled"> @@ -46,12 +46,12 @@ {% endif %} <div class="form-group col"> <label for="user-loginname">{{_("Login Name")}}</label> - <input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname or '' }}" {% if user.uid %}readonly{% endif %}> + <input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname or '' }}" {% if user.id %}readonly{% endif %}> <small class="form-text text-muted"> {{_("Only letters, numbers, dashes (\"-\") and underscores (\"_\") are allowed. At most 32, at least 2 characters. There is a word blocklist. Must be unique.")}} </small> </div> - {% if not user.uid %} + {% if not user.id %} <div class="form-group col"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="ignore-loginname-blocklist" name="ignore-loginname-blocklist" value="1" aria-label="enabled"> @@ -75,7 +75,7 @@ </div> <div class="form-group col"> <label for="user-loginname">{{_("Password")}}</label> - {% if user.uid %} + {% if user.id %} <input type="password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}"> {% else %} <input type="password" class="form-control" id="user-password" name="password" placeholder="{{_("mail to set it will be sent")}}" readonly> @@ -130,9 +130,9 @@ </thead> <tbody> {% for group in user.groups|sort(attribute="name") %} - <tr id="group-{{ group.gid }}"> + <tr id="group-{{ group.id }}"> <td> - <a href="{{ url_for("group.show", gid=group.gid) }}"> + <a href="{{ url_for("group.show", id=group.id) }}"> {{ group.name }} </a> </td> diff --git a/uffd/user/views_group.py b/uffd/user/views_group.py index bfd71c2d..7bbe310b 100644 --- a/uffd/user/views_group.py +++ b/uffd/user/views_group.py @@ -1,8 +1,11 @@ -from flask import Blueprint, render_template, current_app, request -from flask_babel import lazy_gettext +from flask import Blueprint, render_template, current_app, request, flash, redirect, url_for +from flask_babel import lazy_gettext, gettext as _ +import sqlalchemy from uffd.navbar import register_navbar +from uffd.csrf import csrf_protect from uffd.session import login_required +from uffd.database import db from .models import Group @@ -21,6 +24,45 @@ def group_acl(): def index(): return render_template('group/list.html', groups=Group.query.all()) -@bp.route("/<int:gid>") -def show(gid): - return render_template('group/show.html', group=Group.query.filter_by(gid=gid).first_or_404()) +@bp.route("/<int:id>") +@bp.route("/new") +def show(id=None): + group = Group() if id is None else Group.query.get_or_404(id) + return render_template('group/show.html', group=group) + +@bp.route("/<int:id>/update", methods=['POST']) +@bp.route("/new", methods=['POST']) +@csrf_protect(blueprint=bp) +def update(id=None): + if id is None: + group = Group() + if request.form['unix_gid']: + group.unix_gid = int(request.form['unix_gid']) + group.name = request.form['name'] + else: + group = Group.query.get_or_404(id) + group.description = request.form['description'] + db.session.add(group) + if id is None: + try: + db.session.commit() + except sqlalchemy.exc.IntegrityError: + db.session.rollback() + flash(_('Group with this name or id already exists')) + return render_template('group/show.html', group=group), 400 + else: + db.session.commit() + if id is None: + flash(_('Group created')) + else: + flash(_('Group updated')) + return redirect(url_for('group.show', id=group.id)) + +@bp.route("/<int:id>/delete") +@csrf_protect(blueprint=bp) +def delete(id): + group = Group.query.get_or_404(id) + db.session.delete(group) + db.session.commit() + flash(_('Deleted group')) + return redirect(url_for('group.index')) diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py index c98ba361..9baf94a1 100644 --- a/uffd/user/views_user.py +++ b/uffd/user/views_user.py @@ -3,6 +3,7 @@ import io from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app from flask_babel import gettext as _, lazy_gettext +from sqlalchemy.exc import IntegrityError from uffd.navbar import register_navbar from uffd.csrf import csrf_protect @@ -10,7 +11,6 @@ from uffd.selfservice import send_passwordreset from uffd.session import login_required from uffd.role.models import Role from uffd.database import db -from uffd.ldap import ldap, LDAPCommitError from .models import User @@ -31,17 +31,17 @@ def user_acl(): def index(): return render_template('user/list.html', users=User.query.all()) -@bp.route("/<int:uid>") +@bp.route("/<int:id>") @bp.route("/new") -def show(uid=None): - user = User() if uid is None else User.query.filter_by(uid=uid).first_or_404() +def show(id=None): + user = User() if id is None else User.query.get_or_404(id) return render_template('user/show.html', user=user, roles=Role.query.all()) -@bp.route("/<int:uid>/update", methods=['POST']) +@bp.route("/<int:id>/update", methods=['POST']) @bp.route("/new", methods=['POST']) @csrf_protect(blueprint=bp) -def update(uid=None): - if uid is None: +def update(id=None): + if id is None: user = User() ignore_blocklist = request.form.get('ignore-loginname-blocklist', False) if request.form.get('serviceaccount'): @@ -50,30 +50,29 @@ def update(uid=None): flash(_('Login name does not meet requirements')) return redirect(url_for('user.show')) else: - user = User.query.filter_by(uid=uid).first_or_404() + user = User.query.get_or_404(id) if user.mail != request.form['mail'] and not user.set_mail(request.form['mail']): flash(_('Mail is invalid')) - return redirect(url_for('user.show', uid=uid)) + return redirect(url_for('user.show', id=id)) new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname'] if user.displayname != new_displayname and not user.set_displayname(new_displayname): flash(_('Display name does not meet requirements')) - return redirect(url_for('user.show', uid=uid)) + return redirect(url_for('user.show', id=id)) new_password = request.form.get('password') - if uid is not None and new_password: + if id is not None and new_password: if not user.set_password(new_password): flash(_('Password is invalid')) - return redirect(url_for('user.show', uid=uid)) - ldap.session.add(user) + return redirect(url_for('user.show', id=id)) + db.session.add(user) user.roles.clear() for role in Role.query.all(): if not user.is_service_user and role.is_default: continue if request.values.get('role-{}'.format(role.id), False): - user.roles.add(role) + user.roles.append(role) user.update_groups() - ldap.session.commit() db.session.commit() - if uid is None: + if id is None: if user.is_service_user: flash(_('Service user created')) else: @@ -81,15 +80,14 @@ def update(uid=None): flash(_('User created. We sent the user a password reset link by mail')) else: flash(_('User updated')) - return redirect(url_for('user.show', uid=user.uid)) + return redirect(url_for('user.show', id=user.id)) -@bp.route("/<int:uid>/del") +@bp.route("/<int:id>/del") @csrf_protect(blueprint=bp) -def delete(uid): - user = User.query.filter_by(uid=uid).first_or_404() +def delete(id): + user = User.query.get_or_404(id) user.roles.clear() - ldap.session.delete(user) - ldap.session.commit() + db.session.delete(user) db.session.commit() flash(_('Deleted user')) return redirect(url_for('user.index')) @@ -119,17 +117,15 @@ def csvimport(): if not newuser.set_mail(row[1]): flash("invalid mail address, skipped : {}".format(row)) continue - ldap.session.add(newuser) + db.session.add(newuser) for role in roles: if str(role.id) in row[2].split(';'): - role.members.add(newuser) + role.members.append(newuser) newuser.update_groups() try: - ldap.session.commit() db.session.commit() - except LDAPCommitError: + except IntegrityError: flash('Error adding user {}'.format(row[0])) - ldap.session.rollback() db.session.rollback() continue send_passwordreset(newuser, new=True) -- GitLab