From b086d41607628635e242a8a80631dfe259fcc75f Mon Sep 17 00:00:00 2001 From: William Brown Date: Jan 18 2018 04:01:47 +0000 Subject: Ticket 49425 - improve demo objects for install Bug Description: Improve demo objects for install Fix Description: Change the tree a tiny bit - add hidden 389 container, add ous, use better aci examples. This also adds a set of tests to assert the default aci's work as advertised. https://pagure.io/389-ds-base/issue/49425 Author: wibrown Review by: mreynolds, spichugi, tbordaz, lkrispen (Thank you all!) --- diff --git a/ldap/schema/30ns-common.ldif b/ldap/schema/30ns-common.ldif index 2b6edc1..80b8cf6 100644 --- a/ldap/schema/30ns-common.ldif +++ b/ldap/schema/30ns-common.ldif @@ -54,6 +54,8 @@ attributeTypes: ( nsLogSuppress-oid NAME 'nsLogSuppress' DESC 'Netscape defined attributeTypes: ( nsJarfilename-oid NAME 'nsJarfilename' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape' ) attributeTypes: ( nsClassname-oid NAME 'nsClassname' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape' ) attributeTypes: ( 2.16.840.1.113730.3.1.2337 NAME 'nsCertSubjectDN' DESC 'An x509 DN from a certificate used to map during a TLS bind process' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN '389 Directory Server Project' ) +attributeTypes: ( 2.16.840.1.113730.3.1.2342 NAME 'nsSshPublicKey' DESC 'An nsSshPublicKey record' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN '389 Directory Server Project' ) +attributeTypes: ( 2.16.840.1.113730.3.1.2343 NAME 'legalName' DESC 'An individuals legalName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server Project' ) objectClasses: ( nsAdminDomain-oid NAME 'nsAdminDomain' DESC 'Netscape defined objectclass' SUP organizationalUnit MAY ( nsAdminDomainName ) X-ORIGIN 'Netscape' ) objectClasses: ( nsHost-oid NAME 'nsHost' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( serverHostName $ description $ l $ nsHostLocation $ nsHardwarePlatform $ nsOsVersion ) X-ORIGIN 'Netscape' ) objectClasses: ( nsAdminGroup-oid NAME 'nsAdminGroup' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( nsAdminGroupName $ description $ nsConfigRoot $ nsAdminSIEDN ) X-ORIGIN 'Netscape' ) @@ -65,4 +67,11 @@ objectClasses: ( nsAdminObject-oid NAME 'nsAdminObject' DESC 'Netscape defined o objectClasses: ( nsConfig-oid NAME 'nsConfig' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( description $ nsServerPort $ nsServerAddress $ nsSuiteSpotUser $ nsErrorLog $ nsPidLog $ nsAccessLog $ nsDefaultAcceptLanguage $ nsServerSecurity ) X-ORIGIN 'Netscape' ) objectClasses: ( nsDirectoryInfo-oid NAME 'nsDirectoryInfo' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( nsBindDN $ nsBindPassword $ nsDirectoryURL $ nsDirectoryFailoverList $ nsDirectoryInfoRef ) X-ORIGIN 'Netscape' ) objectClasses: ( 2.16.840.1.113730.3.2.329 NAME 'nsMemberOf' DESC 'Allow memberOf assignment on groups for nesting and users' SUP top AUXILIARY MAY ( memberOf ) X-ORIGIN '389 Directory Server Project' ) -objectClasses: ( 2.16.840.1.113730.3.2.331 NAME 'nsAccount' DESC 'A representation of a user in a directory server' SUP top AUXILIARY MAY ( userCertificate $ nsCertSubjectDN ) X-ORIGIN '389 Directory Server Project' ) +objectClasses: ( 2.16.840.1.113730.3.2.331 NAME 'nsAccount' DESC 'A representation of a binding user in a directory server' SUP top AUXILIARY MAY ( userCertificate $ nsCertSubjectDN $ nsSshPublicKey $ userPassword ) X-ORIGIN '389 Directory Server Project' ) +objectClasses: ( 2.16.840.1.113730.3.2.333 NAME 'nsPerson' DESC 'A representation of a person in a directory server' SUP top STRUCTURAL MUST ( displayName $ cn ) MAY ( userPassword $ seeAlso $ description $ legalName $ mail $ preferredLanguage ) X-ORIGIN '389 Directory Server Project' ) +objectClasses: ( 2.16.840.1.113730.3.2.334 NAME 'nsOrgPerson' DESC 'A representation of an org person in directory server. See also inetOrgPerson.' SUP top AUXILIARY MAY ( + businessCategory $ carLicense $ departmentNumber $ employeeNumber $ employeeType $ + homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ manager $ + mobile $ o $ pager $ photo $ roomNumber $ uid $ userCertificate $ telephoneNumber $ + x500uniqueIdentifier $ userSMIMECertificate $ userPKCS12 ) + X-ORIGIN '389 Directory Server Project' ) diff --git a/src/lib389/lib389/cli_idm/user.py b/src/lib389/lib389/cli_idm/user.py index 70e7148..b2f71d9 100644 --- a/src/lib389/lib389/cli_idm/user.py +++ b/src/lib389/lib389/cli_idm/user.py @@ -7,7 +7,7 @@ # --- END COPYRIGHT BLOCK --- import argparse -from lib389.idm.user import UserAccount, UserAccounts, MUST_ATTRIBUTES +from lib389.idm.user import nsUserAccount, nsUserAccounts from lib389.cli_base import ( populate_attr_arguments, @@ -22,8 +22,8 @@ from lib389.cli_base import ( _warn, ) -SINGULAR = UserAccount -MANY = UserAccounts +SINGULAR = nsUserAccount +MANY = nsUserAccounts RDN = 'uid' # These are a generic specification, try not to tamper with them @@ -40,7 +40,7 @@ def get_dn(inst, basedn, log, args): _generic_get_dn(inst, basedn, log.getChild('_generic_get_dn'), MANY, dn) def create(inst, basedn, log, args): - kwargs = _get_attributes(args, MUST_ATTRIBUTES) + kwargs = _get_attributes(args, SINGULAR._must_attributes) _generic_create(inst, basedn, log.getChild('_generic_create'), MANY, kwargs) def delete(inst, basedn, log, args, warn=True): @@ -51,7 +51,7 @@ def delete(inst, basedn, log, args, warn=True): def status(inst, basedn, log, args): uid = _get_arg( args.uid, msg="Enter %s to check" % RDN) - uas = UserAccounts(inst, basedn) + uas = MANY(inst, basedn) acct = uas.get(uid) acct_str = "locked: %s" % acct.is_locked() log.info('uid: %s' % uid) @@ -59,14 +59,14 @@ def status(inst, basedn, log, args): def lock(inst, basedn, log, args): uid = _get_arg( args.uid, msg="Enter %s to check" % RDN) - accounts = UserAccounts(inst, basedn) + accounts = MANY(inst, basedn) acct = accounts.get(uid) acct.lock() log.info('locked %s' % uid) def unlock(inst, basedn, log, args): uid = _get_arg( args.uid, msg="Enter %s to check" % RDN) - accounts = UserAccounts(inst, basedn) + accounts = MANY(inst, basedn) acct = accounts.get(uid) acct.unlock() log.info('unlocked %s' % uid) diff --git a/src/lib389/lib389/configurations/__init__.py b/src/lib389/lib389/configurations/__init__.py index c7b9067..434147a 100644 --- a/src/lib389/lib389/configurations/__init__.py +++ b/src/lib389/lib389/configurations/__init__.py @@ -6,20 +6,30 @@ # See LICENSE for details. # --- END COPYRIGHT BLOCK --- +from lib389.utils import ds_is_newer from lib389._constants import INSTALL_LATEST_CONFIG from .config_001003006 import c001003006, c001003006_sample_entries +from .config_001004000 import c001004000, c001004000_sample_entries def get_config(version): - if (version == INSTALL_LATEST_CONFIG): + # We do this to avoid test breaking on older version that may + # not expect the new default layout. + if (version == INSTALL_LATEST_CONFIG and ds_is_newer('1.4.0')): + return c001004000 + elif (version == INSTALL_LATEST_CONFIG): return c001003006 - if (version == '001003006'): + elif (version == '001004000' and ds_is_newer('1.4.0')): + return c001004000 + elif (version == '001003006'): return c001003006 raise Exception('version %s no match' % version) def get_sample_entries(version): if (version == INSTALL_LATEST_CONFIG): - return c001003006_sample_entries - if (version == '001003006'): + return c001004000_sample_entries + elif (version == '001004000'): + return c001004000_sample_entries + elif (version == '001003006'): return c001003006_sample_entries raise Exception('version %s no match' % version) diff --git a/src/lib389/lib389/configurations/config_001004000.py b/src/lib389/lib389/configurations/config_001004000.py new file mode 100644 index 0000000..36b89e9 --- /dev/null +++ b/src/lib389/lib389/configurations/config_001004000.py @@ -0,0 +1,151 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +from ldap import dn + +from .config import baseconfig, configoperation +from .sample import sampleentries + +from lib389.idm.domain import Domain +from lib389.idm.organisationalunit import OrganisationalUnits +from lib389.idm.group import Groups +from lib389.idm.posixgroup import PosixGroups +from lib389.idm.user import nsUserAccounts +from lib389.idm.services import ServiceAccounts + +from lib389.idm.nscontainer import nsHiddenContainers + +class c001004000_sample_entries(sampleentries): + def __init__(self, instance, basedn): + super(c001004000_sample_entries, self).__init__(instance, basedn) + self.description = """Apply sample entries matching the 1.4.0 sample data and access controls""" + + # All checks done, apply! + def _apply(self): + # Create the base domain object + domain = Domain(self._instance, dn=self._basedn) + # Explode the dn to get the first bit. + avas = dn.str2dn(self._basedn) + dc_ava = avas[0][0][1] + + domain.create(properties={ + # I think in python 2 this forces unicode return ... + 'dc': dc_ava, + 'description': self._basedn, + 'aci': [ + # Allow reading the base domain object + '(targetattr="dc || description || objectClass")(targetfilter="(objectClass=domain)")(version 3.0; acl "Enable anyone domain read"; allow (read, search, compare)(userdn="ldap:///anyone");)', + # Allow reading the ou + '(targetattr="ou || objectClass")(targetfilter="(objectClass=organizationalUnit)")(version 3.0; acl "Enable anyone ou read"; allow (read, search, compare)(userdn="ldap:///anyone");)' + ] + }) + + # Create the 389 service container + # This could also move to be part of core later .... + hidden_containers = nsHiddenContainers(self._instance, self._basedn) + ns389container = hidden_containers.create(properties={ + 'cn': '389_ds_system' + }) + + # Create our ous. + ous = OrganisationalUnits(self._instance, self._basedn) + ous.create(properties = { + 'ou': 'groups', + 'aci': [ + # Allow anon partial read + '(targetattr="cn || member || gidNumber || nsUniqueId || description || objectClass")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable anyone group read"; allow (read, search, compare)(userdn="ldap:///anyone");)', + # Allow group_modify to modify but not create groups + '(targetattr="member")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable group_modify to alter members"; allow (write)(groupdn="ldap:///cn=group_modify,ou=permissions,{BASEDN}");)'.format(BASEDN=self._basedn), + # Allow group_admin to fully manage groups (posix or not). + '(targetattr="cn || member || gidNumber || description || objectClass")(targetfilter="(objectClass=groupOfNames)")(version 3.0; acl "Enable group_admin to manage groups"; allow (write, add, delete)(groupdn="ldap:///cn=group_admin,ou=permissions,{BASEDN}");)'.format(BASEDN=self._basedn), + ] + }) + + ous.create(properties = { + 'ou': 'people', + 'aci': [ + # allow anon partial read. + '(targetattr="objectClass || description || nsUniqueId || uid || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || nsSshPublicKey || nsAccountLock || userCertificate")(targetfilter="(objectClass=posixaccount)")(version 3.0; acl "Enable anyone user read"; allow (read, search, compare)(userdn="ldap:///anyone");)', + # allow self partial mod + '(targetattr="displayName || nsSshPublicKey")(version 3.0; acl "Enable self partial modify"; allow (write)(userdn="ldap:///self");)', + # Allow self full read + '(targetattr="legalName || telephoneNumber || mobile")(targetfilter="(objectClass=nsPerson)")(version 3.0; acl "Enable self legalname read"; allow (read, search, compare)(userdn="ldap:///self");)', + # Allow reading legal name + '(targetattr="legalName || telephoneNumber")(targetfilter="(objectClass=nsPerson)")(version 3.0; acl "Enable user legalname read"; allow (read, search, compare)(groupdn="ldap:///cn=user_private_read,ou=permissions,{BASEDN}");)'.format(BASEDN=self._basedn), + # These below need READ so they can read userPassword and legalName + # Allow user admin create mod + '(targetattr="uid || description || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || legalName || telephoneNumber || mobile")(targetfilter="(&(objectClass=nsPerson)(objectClass=nsAccount))")(version 3.0; acl "Enable user admin create"; allow (write, add, delete, read)(groupdn="ldap:///cn=user_admin,ou=permissions,{BASEDN}");)'.format(BASEDN=self._basedn), + # Allow user mod mod only + '(targetattr="uid || description || displayName || loginShell || uidNumber || gidNumber || gecos || homeDirectory || cn || memberOf || mail || legalName || telephoneNumber || mobile")(targetfilter="(&(objectClass=nsPerson)(objectClass=nsAccount))")(version 3.0; acl "Enable user modify to change users"; allow (write, read)(groupdn="ldap:///cn=user_modify,ou=permissions,{BASEDN}");)'.format(BASEDN=self._basedn), + # Allow user_pw_admin to nsaccountlock and password + '(targetattr="userPassword || nsAccountLock || userCertificate || nsSshPublicKey")(targetfilter="(objectClass=nsAccount)")(version 3.0; acl "Enable user password reset"; allow (write, read)(groupdn="ldap:///cn=user_passwd_reset,ou=permissions,{BASEDN}");)'.format(BASEDN=self._basedn), + ] + }) + + ous.create(properties = { + 'ou': 'permissions', + }) + + ous.create(properties = { + 'ou': 'services', + 'aci': [ + # Minimal service read + '(targetattr="objectClass || description || nsUniqueId || cn || memberOf || nsAccountLock ")(targetfilter="(objectClass=netscapeServer)")(version 3.0; acl "Enable anyone service account read"; allow (read, search, compare)(userdn="ldap:///anyone");)', + ] + }) + + # Create the demo user + users = nsUserAccounts(self._instance, self._basedn) + users.create(properties={ + 'uid': 'demo_user', + 'cn': 'Demo User', + 'displayName': 'Demo User', + 'legalName': 'Demo User Name', + 'uidNumber': '99998', + 'gidNumber': '99998', + 'homeDirectory': '/var/empty', + 'loginShell': '/bin/false', + 'nsAccountlock': 'true' + }) + + # Create the demo group + groups = PosixGroups(self._instance, self._basedn) + groups.create(properties={ + 'cn' : 'demo_group', + 'gidNumber': '99999' + }) + + # Create the permission groups required for the acis + permissions = Groups(self._instance, self._basedn, rdn='ou=permissions') + permissions.create(properties={ + 'cn': 'group_admin', + }) + permissions.create(properties={ + 'cn': 'group_modify', + }) + permissions.create(properties={ + 'cn': 'user_admin', + }) + permissions.create(properties={ + 'cn': 'user_modify', + }) + permissions.create(properties={ + 'cn': 'user_passwd_reset', + }) + permissions.create(properties={ + 'cn': 'user_private_read', + }) + + +class c001004000(baseconfig): + def __init__(self, instance): + super(c001004000, self).__init__(instance) + self._operations = [ + # For now this is an empty place holder - likely this + # will become part of core server. + ] diff --git a/src/lib389/lib389/idm/account.py b/src/lib389/lib389/idm/account.py index 92038bf..100bfec 100644 --- a/src/lib389/lib389/idm/account.py +++ b/src/lib389/lib389/idm/account.py @@ -93,6 +93,22 @@ class Account(DSLdapObject): inst_clone.open(saslmethod='gssapi') return inst_clone + def enroll_certificate(self, der_path): + """Enroll a certificate for certmap verification. Because of the userCertificate + attribute, we have to do this on userAccount which has support for it. + + :param der_path: the certificate file in DER format to include. + :type der_path: str + """ + if ds_is_older('1.4.0'): + raise Exception("This version of DS does not support nsAccount") + # Given a cert path, add this to the object as a userCertificate + crt = None + with open(der_path, 'rb') as f: + crt = f.read() + self.add('usercertificate;binary', crt) + + class Accounts(DSLdapObjects): """DSLdapObjects that represents Account entry @@ -106,9 +122,12 @@ class Accounts(DSLdapObjects): super(Accounts, self).__init__(instance) # These are all the objects capable of holding a password. self._objectclasses = [ + 'nsAccount', + 'nsPerson', 'simpleSecurityObject', 'organization', - 'personperson', + 'person', + 'account', 'organizationalUnit', 'netscapeServer', 'domain', diff --git a/src/lib389/lib389/idm/user.py b/src/lib389/lib389/idm/user.py index c4d7928..38977a5 100644 --- a/src/lib389/lib389/idm/user.py +++ b/src/lib389/lib389/idm/user.py @@ -30,10 +30,89 @@ TEST_USER_PROPERTIES = { 'homeDirectory' : '/home/testuser' } +#### Modern userAccounts + +class nsUserAccount(Account): + _must_attributes = [ + 'uid', + 'cn', + 'displayName', + 'uidNumber', + 'gidNumber', + 'homeDirectory', + ] + + """A single instance of an nsPerson, capable of posix login, certificate + authentication, sshkey distribution, and more. + + This is the modern and correct userAccount type to choose for DS 1.4.0 and above. + + :param instance: An instance + :type instance: lib389.DirSrv + :param dn: Entry DN + :type dn: str + """ + def __init__(self, instance, dn=None): + if ds_is_older('1.4.0'): + raise Exception("Not supported") + super(nsUserAccount, self).__init__(instance, dn) + self._rdn_attribute = RDN + self._must_attributes = nsUserAccount._must_attributes + # Can I generate these from schema? + self._create_objectclasses = [ + 'top', + 'nsPerson', + 'nsAccount', + 'nsOrgPerson', + 'posixAccount', + ] + user_compare_exclude = [ + 'nsUniqueId', + 'modifyTimestamp', + 'createTimestamp', + 'entrydn' + ] + self._compare_exclude = self._compare_exclude + user_compare_exclude + self._protected = False + +class nsUserAccounts(DSLdapObjects): + """DSLdapObjects that represents all nsUserAccount entries in suffix. + By default it uses 'ou=People' as rdn. + + This is the modern and correct userAccount type to choose for DS 1.4.0 and above. + + :param instance: An instance + :type instance: lib389.DirSrv + :param basedn: Suffix DN + :type basedn: str + :param rdn: The DN that will be combined wit basedn + :type rdn: str + """ + + def __init__(self, instance, basedn, rdn='ou=people'): + super(nsUserAccounts, self).__init__(instance) + self._objectclasses = [ + 'nsPerson', + 'nsAccount', + 'nsOrgPerson', + 'posixAccount', + ] + self._filterattrs = [RDN, 'displayName', 'cn'] + self._childobject = nsUserAccount + if rdn is None: + self._basedn = basedn + else: + self._basedn = '{},{}'.format(rdn, basedn) + + +#### Traditional style userAccounts. class UserAccount(Account): """A single instance of User Account entry + This is the classic "user account" style of cn + sn. You should consider + nsUserAccount instead. + :param instance: An instance :type instance: lib389.DirSrv :param dn: Entry DN @@ -49,12 +128,8 @@ class UserAccount(Account): 'top', 'account', 'posixaccount', - # inetOrgPerson allows userCertificate 'inetOrgPerson', 'organizationalPerson', - # This may not always work at sites? - # Can we get this into core? - # 'ldapPublicKey', ] if ds_is_older('1.3.7'): self._create_objectclasses.append('inetUser') @@ -77,29 +152,14 @@ class UserAccount(Account): return super(UserAccount, self)._validate(rdn, properties, basedn) - def enroll_certificate(self, der_path): - """Enroll a certificate for certmap verification. Because of the userCertificate - attribute, we have to do this on userAccount which has support for it. - - :param der_path: the certificate file in DER format to include. - :type der_path: str - """ - if ds_is_older('1.4.0'): - raise Exception("This version of DS does not support nsAccount") - # Given a cert path, add this to the object as a userCertificate - crt = None - with open(der_path, 'rb') as f: - crt = f.read() - self.add('usercertificate;binary', crt) - - # Add a set password function.... - # Can't I actually just set, and it will hash? - class UserAccounts(DSLdapObjects): """DSLdapObjects that represents all User Account entries in suffix. By default it uses 'ou=People' as rdn. + This is the classic "user account" style of cn + sn. You should consider + nsUserAccounts instead. + :param instance: An instance :type instance: lib389.DirSrv :param basedn: Suffix DN diff --git a/src/lib389/lib389/tests/cli/idm_user_test.py b/src/lib389/lib389/tests/cli/idm_user_test.py index da579b4..537e12d 100644 --- a/src/lib389/lib389/tests/cli/idm_user_test.py +++ b/src/lib389/lib389/tests/cli/idm_user_test.py @@ -44,9 +44,10 @@ def test_user_tasks(topology): # Create the user topology.logcap.flush() - u_args.cn = 'testuser' u_args.uid = 'testuser' - u_args.sn = 'testuser' + # u_args.sn = 'testuser' + u_args.cn = 'Test User' + u_args.displayName = 'Test User' u_args.homeDirectory = '/home/testuser' u_args.uidNumber = '5000' u_args.gidNumber = '5000' diff --git a/src/lib389/lib389/tests/configurations/config_001004000_test.py b/src/lib389/lib389/tests/configurations/config_001004000_test.py new file mode 100644 index 0000000..0cb31ab --- /dev/null +++ b/src/lib389/lib389/tests/configurations/config_001004000_test.py @@ -0,0 +1,231 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# +import ldap + +import logging +import sys +import time + +import pytest + +from lib389 import DirSrv +from lib389._constants import * +from lib389.properties import * + +from lib389.topologies import topology_st + +from lib389.idm.user import nsUserAccounts +from lib389.idm.posixgroup import PosixGroups +from lib389.idm.group import Groups + +from lib389.utils import ds_is_older +pytestmark = pytest.mark.skipif(ds_is_older('1.4.0'), reason="Not implemented") + +REQUIRED_DNS = [ + 'dc=example,dc=com', + 'ou=groups,dc=example,dc=com', + 'ou=people,dc=example,dc=com', + 'ou=services,dc=example,dc=com', + 'ou=permissions,dc=example,dc=com', + 'uid=demo_user,ou=people,dc=example,dc=com', + 'cn=demo_group,ou=groups,dc=example,dc=com', + 'cn=group_admin,ou=permissions,dc=example,dc=com', + 'cn=group_modify,ou=permissions,dc=example,dc=com', + 'cn=user_admin,ou=permissions,dc=example,dc=com', + 'cn=user_modify,ou=permissions,dc=example,dc=com', + 'cn=user_passwd_reset,ou=permissions,dc=example,dc=com', + 'cn=user_private_read,ou=permissions,dc=example,dc=com', +] + +def test_install_sample_entries(topology_st): + """Assert that our entries match.""" + + entries = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE) + + for entry in entries: + assert(entry.dn in REQUIRED_DNS) + # We can make this assert the full object content, plugins and more later. + +def test_install_aci(topology_st): + """Assert our default aci's work as expected.""" + + # Create some users and groups. + users = nsUserAccounts(topology_st.standalone, DEFAULT_SUFFIX) + groups = PosixGroups(topology_st.standalone, DEFAULT_SUFFIX) + + user_basic = users.create(properties={ + 'uid': 'basic', + 'cn': 'Basic', + 'displayName': 'Basic', + 'uidNumber': '100000', + 'gidNumber': '100000', + 'homeDirectory': '/home/basic', + 'userPassword': 'password', + 'legalName': 'Super Secret PII', + }) + + user_modify = users.create(properties={ + 'uid': 'modify', + 'cn': 'Modify', + 'displayName': 'Modify', + 'uidNumber': '100001', + 'gidNumber': '100001', + 'homeDirectory': '/home/modify', + 'userPassword': 'password', + 'legalName': 'Super Secret PII', + }) + + user_admin = users.create(properties={ + 'uid': 'admin', + 'cn': 'Admin', + 'displayName': 'Admin', + 'uidNumber': '100002', + 'gidNumber': '100002', + 'homeDirectory': '/home/admin', + 'userPassword': 'password', + 'legalName': 'Super Secret PII', + }) + + user_pw_reset = users.create(properties={ + 'uid': 'pw_reset', + 'cn': 'Password Reset', + 'displayName': 'Password Reset', + 'uidNumber': '100003', + 'gidNumber': '100003', + 'homeDirectory': '/home/pw_reset', + 'userPassword': 'password', + 'legalName': 'Super Secret PII', + }) + + # Add users to various permissions. + + permissions = Groups(topology_st.standalone, DEFAULT_SUFFIX, rdn='ou=permissions') + + g_group_admin = permissions.get('group_admin') + g_group_modify = permissions.get('group_modify') + g_user_admin = permissions.get('user_admin') + g_user_modify = permissions.get('user_modify') + g_user_pw_reset = permissions.get('user_passwd_reset') + + g_group_admin.add_member(user_admin.dn) + g_user_admin.add_member(user_admin.dn) + + g_group_modify.add_member(user_modify.dn) + g_user_modify.add_member(user_modify.dn) + + g_user_pw_reset.add_member(user_pw_reset.dn) + + # Bind as a user and assert what we can and can not see + c_user_basic = user_basic.bind(password='password') + c_user_modify = user_modify.bind(password='password') + c_user_admin = user_admin.bind(password='password') + c_user_pw_reset = user_pw_reset.bind(password='password') + + c_user_basic_users = nsUserAccounts(c_user_basic, DEFAULT_SUFFIX) + c_user_pw_reset_users = nsUserAccounts(c_user_pw_reset, DEFAULT_SUFFIX) + c_user_modify_users = nsUserAccounts(c_user_modify, DEFAULT_SUFFIX) + c_user_admin_users = nsUserAccounts(c_user_admin, DEFAULT_SUFFIX) + + # Should be able to see users, but not their legalNames + user_basic_view_demo_user = c_user_basic_users.get('demo_user') + assert user_basic_view_demo_user.get_attr_val_utf8('legalName') is None + assert user_basic_view_demo_user.get_attr_val_utf8('uid') == 'demo_user' + + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_basic_view_demo_user.replace('description', 'change value') + + user_pw_reset_view_demo_user = c_user_pw_reset_users.get('demo_user') + assert user_pw_reset_view_demo_user.get_attr_val_utf8('legalName') is None + assert user_pw_reset_view_demo_user.get_attr_val_utf8('uid') == 'demo_user' + + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_pw_reset_view_demo_user.replace('description', 'change value') + + # The user admin and modify should be able to read it. + user_modify_view_demo_user = c_user_modify_users.get('demo_user') + assert user_modify_view_demo_user.get_attr_val_utf8('legalName') == 'Demo User Name' + assert user_modify_view_demo_user.get_attr_val_utf8('uid') == 'demo_user' + user_modify_view_demo_user.replace('description', 'change value') + + user_admin_view_demo_user = c_user_admin_users.get('demo_user') + assert user_admin_view_demo_user.get_attr_val_utf8('legalName') == 'Demo User Name' + assert user_admin_view_demo_user.get_attr_val_utf8('uid') == 'demo_user' + user_admin_view_demo_user.replace('description', 'change value') + + # Assert only admin can create: + + test_user_properties = { + 'uid': 'test_user', + 'cn': 'Test User', + 'displayName': 'Test User', + 'uidNumber': '100005', + 'gidNumber': '100005', + 'homeDirectory': '/home/test_user', + 'legalName': 'Super Secret PII', + } + + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + c_user_basic_users.create(properties=test_user_properties) + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + c_user_pw_reset_users.create(properties=test_user_properties) + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + c_user_modify_users.create(properties=test_user_properties) + + test_user = c_user_admin_users.create(properties=test_user_properties) + test_user.delete() + + # Assert on pw_reset can unlock/pw + + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_basic_view_demo_user.lock() + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_modify_view_demo_user.lock() + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_admin_view_demo_user.lock() + user_pw_reset_view_demo_user.lock() + + # Group test + c_user_basic_groups = PosixGroups(c_user_basic, DEFAULT_SUFFIX) + c_user_pw_reset_groups = PosixGroups(c_user_pw_reset, DEFAULT_SUFFIX) + c_user_modify_groups = PosixGroups(c_user_modify, DEFAULT_SUFFIX) + c_user_admin_groups = PosixGroups(c_user_admin, DEFAULT_SUFFIX) + + # Assert that members can be read, but only modify/admin can edit. + user_basic_view_demo_group = c_user_basic_groups.get('demo_group') + assert user_basic_view_demo_group.get_attr_val_utf8('cn') == 'demo_group' + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_basic_view_demo_group.add_member(user_basic.dn) + + user_pw_reset_view_demo_group = c_user_pw_reset_groups.get('demo_group') + assert user_pw_reset_view_demo_group.get_attr_val_utf8('cn') == 'demo_group' + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + user_pw_reset_view_demo_group.add_member(user_pw_reset.dn) + + user_modify_view_demo_group = c_user_modify_groups.get('demo_group') + assert user_modify_view_demo_group.get_attr_val_utf8('cn') == 'demo_group' + user_modify_view_demo_group.add_member(user_modify.dn) + + user_admin_view_demo_group = c_user_admin_groups.get('demo_group') + assert user_admin_view_demo_group.get_attr_val_utf8('cn') == 'demo_group' + user_admin_view_demo_group.add_member(user_admin.dn) + + # Assert that only admin can add new group. + group_properties = { + 'cn': 'test_group', + 'gidNumber': '100009' + } + + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + c_user_basic_groups.create(properties=group_properties) + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + c_user_pw_reset_groups.create(properties=group_properties) + with pytest.raises(ldap.INSUFFICIENT_ACCESS): + c_user_modify_groups.create(properties=group_properties) + c_user_admin_groups.create(properties=group_properties) +