From 5a26d5458355d175d051f02eadaca8b2e108563d Mon Sep 17 00:00:00 2001 From: William Brown Date: Nov 07 2019 00:56:13 +0000 Subject: Ticket 50641 - Update default aci to allows users to change their own password Bug Description: The default acis were too restrictive - we do want people to be able to self change passwords by default! Fix Description: Fix the default aci's and add tests to prove they behave as we actually expect. https://pagure.io/389-ds-base/pull-request/50641 Author: William Brown Review by: vashirov --- diff --git a/dirsrvtests/tests/suites/acl/default_aci_allows_self_write.py b/dirsrvtests/tests/suites/acl/default_aci_allows_self_write.py new file mode 100644 index 0000000..d82f5eb --- /dev/null +++ b/dirsrvtests/tests/suites/acl/default_aci_allows_self_write.py @@ -0,0 +1,131 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2019 William Brown +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + + +import pytest +from lib389.idm.user import nsUserAccounts, UserAccounts +from lib389.topologies import topology_st as topology +from lib389.utils import ds_is_older +from lib389._constants import * + + +pytestmark = pytest.mark.tier1 + +USER_PASSWORD = "some test password" +NEW_USER_PASSWORD = "some new password" + +@pytest.mark.xfail(ds_is_older('1.4.2.0'), reason="Default aci's in older versions do not support this functionality") +def test_acl_default_allow_self_write_nsuser(topology): + """ + Testing nsusers can self write and self read. This it a sanity test + so that our default entries have their aci's checked. + + :id: 4f0fb01a-36a6-430c-a2ee-ebeb036bd951 + + :setup: Standalone instance + + :steps: + 1. Testing comparison of two different users. + + :expectedresults: + 1. Should fail to compare + """ + topology.standalone.enable_tls() + nsusers = nsUserAccounts(topology.standalone, DEFAULT_SUFFIX) + # Create a user as dm. + user = nsusers.create(properties={ + 'uid': 'test_nsuser', + 'cn': 'test_nsuser', + 'displayName': 'testNsuser', + 'legalName': 'testNsuser', + 'uidNumber': '1001', + 'gidNumber': '1001', + 'homeDirectory': '/home/testnsuser', + 'userPassword': USER_PASSWORD, + }) + # Create a new con and bind as the user. + user_conn = user.bind(USER_PASSWORD) + + user_nsusers = nsUserAccounts(user_conn, DEFAULT_SUFFIX) + self_ent = user_nsusers.get(dn=user.dn) + + # Can we self read x,y,z + check = self_ent.get_attrs_vals_utf8([ + 'uid', + 'cn', + 'displayName', + 'legalName', + 'uidNumber', + 'gidNumber', + 'homeDirectory', + ]) + for k in check.values(): + # Could we read the values? + assert(isinstance(k, list)) + assert(len(k) > 0) + # Can we self change a,b,c + self_ent.ensure_attr_state({ + 'legalName': ['testNsuser_update'], + 'displayName': ['testNsuser_update'], + 'nsSshPublicKey': ['testkey'], + }) + # self change pw + self_ent.change_password(USER_PASSWORD, NEW_USER_PASSWORD) + + +@pytest.mark.xfail(ds_is_older('1.4.2.0'), reason="Default aci's in older versions do not support this functionality") +def test_acl_default_allow_self_write_user(topology): + """ + Testing users can self write and self read. This it a sanity test + so that our default entries have their aci's checked. + + :id: 4c52321b-f473-4c32-a1d5-489b138cd199 + + :setup: Standalone instance + + :steps: + 1. Testing comparison of two different users. + + :expectedresults: + 1. Should fail to compare + """ + topology.standalone.enable_tls() + users = UserAccounts(topology.standalone, DEFAULT_SUFFIX) + # Create a user as dm. + user = users.create(properties={ + 'uid': 'test_user', + 'cn': 'test_user', + 'sn': 'User', + 'uidNumber': '1002', + 'gidNumber': '1002', + 'homeDirectory': '/home/testuser', + 'userPassword': USER_PASSWORD, + }) + # Create a new con and bind as the user. + user_conn = user.bind(USER_PASSWORD) + + user_users = UserAccounts(user_conn, DEFAULT_SUFFIX) + self_ent = user_users.get(dn=user.dn) + # Can we self read x,y,z + check = self_ent.get_attrs_vals_utf8([ + 'uid', + 'cn', + 'sn', + 'uidNumber', + 'gidNumber', + 'homeDirectory', + ]) + for (a, k) in check.items(): + print(a) + # Could we read the values? + assert(isinstance(k, list)) + assert(len(k) > 0) + # Self change pw + self_ent.change_password(USER_PASSWORD, NEW_USER_PASSWORD) + + diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py index 91b3711..62fd0ae 100644 --- a/src/lib389/lib389/backend.py +++ b/src/lib389/lib389/backend.py @@ -421,12 +421,14 @@ class Backend(DSLdapObject): :type version: str """ - self._log.debug('Creating sample entries at version %s....' % version) - # Grab the correct sample entry config + self._log.debug('Requested sample entries at version %s....' % version) + # Grab the correct sample entry config - remember this is a function ptr. centries = get_sample_entries(version) # apply it. basedn = self.get_attr_val('nsslapd-suffix') cent = centries(self._instance, basedn) + # Now it's built, we can get the version for logging. + self._log.debug('Creating sample entries at version %s' % cent.version) cent.apply() def _validate(self, rdn, properties, basedn): diff --git a/src/lib389/lib389/configurations/__init__.py b/src/lib389/lib389/configurations/__init__.py index 94ca3aa..da6fc35 100644 --- a/src/lib389/lib389/configurations/__init__.py +++ b/src/lib389/lib389/configurations/__init__.py @@ -10,15 +10,20 @@ 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 +from .config_001004002 import c001004002, c001004002_sample_entries def get_config(version): # 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')): + if (version == INSTALL_LATEST_CONFIG and ds_is_newer('1.4.2')): + return c001004002 + elif (version == INSTALL_LATEST_CONFIG and ds_is_newer('1.4.0')): return c001004000 elif (version == INSTALL_LATEST_CONFIG): return c001003006 + elif (version == '001004002' and ds_is_newer('1.4.2')): + return c001004002 elif (version == '001004000' and ds_is_newer('1.4.0')): return c001004000 elif (version == '001003006'): @@ -28,7 +33,9 @@ def get_config(version): def get_sample_entries(version): if (version == INSTALL_LATEST_CONFIG): - return c001004000_sample_entries + return c001004002_sample_entries + elif (version == '001004002'): + return c001004002_sample_entries elif (version == '001004000'): return c001004000_sample_entries elif (version == '001003006'): diff --git a/src/lib389/lib389/configurations/config_001003006.py b/src/lib389/lib389/configurations/config_001003006.py index d04caaa..df7dba2 100644 --- a/src/lib389/lib389/configurations/config_001003006.py +++ b/src/lib389/lib389/configurations/config_001003006.py @@ -18,6 +18,7 @@ class c001003006_sample_entries(sampleentries): def __init__(self, instance, basedn): super(c001003006_sample_entries, self).__init__(instance, basedn) self.description = """Apply sample entries matching the 1.3.6 sample data and access controls.""" + self.version = "c001003006" # All the checks are done, apply them. def _apply(self): diff --git a/src/lib389/lib389/configurations/config_001004000.py b/src/lib389/lib389/configurations/config_001004000.py index dbeab1f..d5ee3f6 100644 --- a/src/lib389/lib389/configurations/config_001004000.py +++ b/src/lib389/lib389/configurations/config_001004000.py @@ -26,6 +26,7 @@ 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""" + self.version = "c001004000" # All checks done, apply! def _apply(self): diff --git a/src/lib389/lib389/configurations/config_001004002.py b/src/lib389/lib389/configurations/config_001004002.py new file mode 100644 index 0000000..4d031e7 --- /dev/null +++ b/src/lib389/lib389/configurations/config_001004002.py @@ -0,0 +1,140 @@ +# --- 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 .config import baseconfig, configoperation +from .sample import sampleentries, create_base_domain + +from lib389.idm.organizationalunit import OrganizationalUnits +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 c001004002_sample_entries(sampleentries): + def __init__(self, instance, basedn): + super(c001004002_sample_entries, self).__init__(instance, basedn) + self.description = """Apply sample entries matching the 1.4.2 sample data and access controls""" + self.version = "c001004002" + + # All checks done, apply! + def _apply(self): + # Create the base domain object + domain = create_base_domain(self._instance, self._basedn) + domain.add('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 = OrganizationalUnits(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 || legalName || userPassword || nsSshPublicKey")(version 3.0; acl "Enable self partial modify"; allow (write)(userdn="ldap:///self");)', + # Allow self full read + '(targetattr="legalName || telephoneNumber || mobile || sn")(targetfilter="(|(objectClass=nsPerson)(objectClass=inetOrgPerson))")(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 permission to 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 c001004002(baseconfig): + def __init__(self, instance): + super(c001004002, 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/configurations/sample.py b/src/lib389/lib389/configurations/sample.py index f30b8d6..dbc4d8b 100644 --- a/src/lib389/lib389/configurations/sample.py +++ b/src/lib389/lib389/configurations/sample.py @@ -20,6 +20,7 @@ class sampleentries(object): self._instance = instance self._basedn = ensure_str(basedn) self.description = None + self.version = None def apply(self): self._apply()