Add Domain Level feature

https://fedorahosted.org/freeipa/ticket/5018

Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Reviewed-By: Petr Vobornik <pvoborni@redhat.com>
This commit is contained in:
Tomas Babej
2015-05-14 10:49:55 +02:00
committed by Jan Cholasta
parent 9eedffdfa6
commit f3010498af
17 changed files with 280 additions and 14 deletions

View File

@@ -322,6 +322,8 @@ dn: cn=dna,cn=ipa,cn=etc,dc=ipa,dc=example
aci: (targetattr = "cn || createtimestamp || dnahostname || dnaportnum || dnaremainingvalues || dnaremotebindmethod || dnaremoteconnprotocol || dnasecureportnum || entryusn || modifytimestamp || objectclass")(targetfilter = "(objectclass=dnasharedconfig)")(version 3.0;acl "permission:System: Read DNA Configuration";allow (compare,read,search) userdn = "ldap:///all";)
dn: ou=profile,dc=ipa,dc=example
aci: (targetattr = "attributemap || authenticationmethod || bindtimelimit || cn || createtimestamp || credentiallevel || defaultsearchbase || defaultsearchscope || defaultserverlist || dereferencealiases || entryusn || followreferrals || modifytimestamp || objectclass || objectclassmap || ou || preferredserverlist || profilettl || searchtimelimit || serviceauthenticationmethod || servicecredentiallevel || servicesearchdescriptor")(targetfilter = "(|(objectclass=organizationalUnit)(objectclass=DUAConfigProfile))")(version 3.0;acl "permission:System: Read DUA Profile";allow (compare,read,search) userdn = "ldap:///anyone";)
dn: cn=Domain Level,cn=ipa,cn=etc,dc=ipa,dc=example
aci: (targetattr = "createtimestamp || entryusn || ipadomainlevel || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipadomainlevelconfig)")(version 3.0;acl "permission:System: Read Domain Level";allow (compare,read,search) userdn = "ldap:///all";)
dn: cn=masters,cn=ipa,cn=etc,dc=ipa,dc=example
aci: (targetattr = "cn || createtimestamp || entryusn || ipaconfigstring || modifytimestamp || objectclass")(targetfilter = "(objectclass=nscontainer)")(version 3.0;acl "permission:System: Read IPA Masters";allow (compare,read,search) groupdn = "ldap:///cn=System: Read IPA Masters,cn=permissions,cn=pbac,dc=ipa,dc=example";)
dn: cn=config

View File

@@ -1283,6 +1283,15 @@ option: Str('version?', exclude='webui')
output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: PrimaryKey('value', None, None)
command: domainlevel_get
args: 0,1,1
option: Str('version?', exclude='webui')
output: Output('result', <type 'int'>, None)
command: domainlevel_set
args: 1,1,1
arg: Int('ipadomainlevel', cli_name='level', minvalue=0)
option: Str('version?', exclude='webui')
output: Output('result', <type 'int'>, None)
command: env
args: 1,3,4
arg: Str('variables*')

View File

@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
# #
########################################################
IPA_API_VERSION_MAJOR=2
IPA_API_VERSION_MINOR=119
# Last change: edewata - Added vault plugin
IPA_API_VERSION_MINOR=120
# Last change: tbabej - Add Domain Level feature

View File

@@ -0,0 +1,6 @@
dn: cn=schema
attributeTypes: (2.16.840.1.113730.3.8.19.2.1 NAME 'ipaDomainLevel' DESC 'Domain Level value' EQUALITY numericStringMatch ORDERING numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 SINGLE-VALUE X-ORIGIN 'IPA v4')
attributeTypes: (2.16.840.1.113730.3.8.19.2.2 NAME 'ipaMinDomainLevel' DESC 'Minimal supported Domain Level value' EQUALITY numericStringMatch ORDERING numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 SINGLE-VALUE X-ORIGIN 'IPA v4')
attributeTypes: (2.16.840.1.113730.3.8.19.2.3 NAME 'ipaMaxDomainLevel' DESC 'Maximal supported Domain Level value' EQUALITY numericStringMatch ORDERING numericStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 SINGLE-VALUE X-ORIGIN 'IPA v4')
objectClasses: (2.16.840.1.113730.3.8.19.1.1 NAME 'ipaDomainLevelConfig' SUP ipaConfigObject AUXILIARY DESC 'Domain Level Configuration' MUST (ipaDomainLevel) X-ORIGIN 'IPA v4')
objectClasses: (2.16.840.1.113730.3.8.19.1.2 NAME 'ipaSupportedDomainLevelConfig' SUP ipaConfigObject AUXILIARY DESC 'Supported Domain Level Configuration' MUST (ipaMinDomainLevel $ ipaMaxDomainLevel) X-ORIGIN 'IPA v4')

View File

@@ -22,6 +22,7 @@ app_DATA = \
70ipaotp.ldif \
70topology.ldif \
71idviews.ldif \
72domainlevels.ldif \
anonymous-vlv.ldif \
bootstrap-template.ldif \
caJarSigningCert.cfg.template \
@@ -34,6 +35,7 @@ app_DATA = \
ds-nfiles.ldif \
dns.ldif \
dnssec.ldif \
domainlevel.ldif \
kerberos.ldif \
indices.ldif \
bind.named.conf.template \

View File

@@ -0,0 +1,7 @@
# Create default Domain Level for new masters
dn: cn=Domain Level,cn=ipa,cn=etc,$SUFFIX
changetype: add
objectClass: top
objectClass: nsContainer
objectClass: ipaDomainLevelConfig
ipaDomainLevel: $DOMAIN_LEVEL

View File

@@ -3,5 +3,9 @@ changetype: add
objectclass: top
objectclass: nsContainer
objectclass: ipaReplTopoManagedServer
ipaReplTopoManagedSuffix: $SUFFIX
objectClass: ipaConfigObject
objectClass: ipaSupportedDomainLevelConfig
cn: $FQDN
ipaReplTopoManagedSuffix: $SUFFIX
ipaMinDomainLevel: $MIN_DOMAIN_LEVEL
ipaMaxDomainLevel: $MAX_DOMAIN_LEVEL

View File

@@ -43,7 +43,7 @@ from ipaserver.install import cainstance
from ipaserver.install import kra
from ipaserver.install import dns as dns_installer
from ipalib import api, create_api, errors, util, certstore, x509
from ipalib.constants import CACERT
from ipalib import constants
from ipapython import version
from ipapython.config import IPAOptionParser
from ipapython import sysrestore
@@ -224,12 +224,12 @@ def install_ca_cert(ldap, base_dn, realm, cafile):
try:
certs = certstore.get_ca_certs(ldap, base_dn, realm, False)
except errors.NotFound:
shutil.copy(cafile, CACERT)
shutil.copy(cafile, constants.CACERT)
else:
certs = [c[0] for c in certs if c[2] is not False]
x509.write_certificate_list(certs, CACERT)
x509.write_certificate_list(certs, constants.CACERT)
os.chmod(CACERT, 0444)
os.chmod(constants.CACERT, 0444)
except Exception, e:
print "error copying files: " + str(e)
sys.exit(1)
@@ -569,6 +569,30 @@ def main():
print " %% ipa-replica-manage del %s --force" % config.host_name
exit(3)
# Detect the current domain level
try:
current = remote_api.Command['domainlevel_get']()['result']
except errors.NotFound:
# If we're joining an older master, domain entry is not
# available
current = 0
# Detect if current level is out of supported range
# for this IPA version
under_lower_bound = current < constants.MIN_DOMAIN_LEVEL
above_upper_bound = current > constants.MAX_DOMAIN_LEVEL
if under_lower_bound or above_upper_bound:
message = ("This version of FreeIPA does not support "
"the Domain Level which is currently set for "
"this domain. The Domain Level needs to be "
"raised before installing a replica with "
"this version is allowed to be installed "
"within this domain.")
root_logger.error(message)
print(message)
exit(3)
# Check pre-existing host entry
try:
entry = conn.find_entries(u'fqdn=%s' % config.host_name, ['fqdn'], DN(api.env.container_host, api.env.basedn))

View File

@@ -70,7 +70,7 @@ from ipapython import sysrestore
from ipapython.ipautil import *
from ipapython import ipautil
from ipapython import dogtag
from ipalib import api, errors, util, x509
from ipalib import api, errors, util, x509, constants
from ipapython.config import IPAOptionParser
from ipalib.util import validate_domain_name
from ipalib.constants import CACERT
@@ -176,6 +176,8 @@ def parse_options():
help="create home directories for users "
"on their first login")
basic_group.add_option("--hostname", dest="host_name", help="fully qualified name of server")
basic_group.add_option("--domain-level", dest="domainlevel", help="IPA domain level",
default=constants.MAX_DOMAIN_LEVEL, type=int)
basic_group.add_option("--ip-address", dest="ip_addresses",
type="ip", ip_local=True, action="append", default=[],
help="Master Server IP Address. This option can be used multiple times",
@@ -327,6 +329,15 @@ def parse_options():
except ValueError, e:
parser.error("invalid domain: " + unicode(e))
# Check that Domain Level is within the allowed range
if not options.uninstall:
if options.domainlevel < constants.MIN_DOMAIN_LEVEL:
parser.error("Domain Level cannot be lower than {0}"
.format(constants.MIN_DOMAIN_LEVEL))
elif options.domainlevel > constants.MAX_DOMAIN_LEVEL:
parser.error("Domain Level cannot be higher than {0}"
.format(constants.MAX_DOMAIN_LEVEL))
if not options.setup_dns:
if options.forwarders:
parser.error("You cannot specify a --forwarder option without the --setup-dns option")
@@ -1143,21 +1154,24 @@ def main():
ntp.create_instance()
if options.dirsrv_cert_files:
ds = dsinstance.DsInstance(fstore=fstore)
ds = dsinstance.DsInstance(fstore=fstore,
domainlevel=options.domainlevel)
ds.create_instance(realm_name, host_name, domain_name,
dm_password, dirsrv_pkcs12_info,
idstart=options.idstart, idmax=options.idmax,
subject_base=options.subject,
hbac_allow=not options.hbac_allow)
else:
ds = dsinstance.DsInstance(fstore=fstore)
ds = dsinstance.DsInstance(fstore=fstore,
domainlevel=options.domainlevel)
ds.create_instance(realm_name, host_name, domain_name,
dm_password,
idstart=options.idstart, idmax=options.idmax,
subject_base=options.subject,
hbac_allow=not options.hbac_allow)
else:
ds = dsinstance.DsInstance(fstore=fstore)
ds = dsinstance.DsInstance(fstore=fstore,
domainlevel=options.domainlevel)
ds.init_info(
realm_name, host_name, domain_name, dm_password,
options.subject, 1101, 1100, None)

View File

@@ -0,0 +1,14 @@
# Create default Domain Level entry if it does not exist
dn: cn=Domain Level,cn=ipa,cn=etc,$SUFFIX
default: objectClass: top
default: objectClass: nsContainer
default: objectClass: ipaDomainLevelConfig
default: ipaDomainLevel: 0
# Create entry proclaiming Domain Level support of this master
# This will update the supported Domain Levels during upgrade
dn: cn=$FQDN,cn=masters,cn=ipa,cn=etc,$SUFFIX
add: objectClass: ipaConfigObject
add: objectClass: ipaSupportedDomainLevelConfig
only: ipaMinDomainLevel: $MIN_DOMAIN_LEVEL
only: ipaMaxDomainLevel: $MAX_DOMAIN_LEVEL

View File

@@ -49,6 +49,7 @@ app_DATA = \
61-trusts-s4u2proxy.update \
62-ranges.update \
71-idviews.update \
72-domainlevels.update \
90-post_upgrade_plugins.update \
$(NULL)

View File

@@ -224,3 +224,6 @@ LDAP_GENERALIZED_TIME_FORMAT = "%Y%m%d%H%M%SZ"
IPA_ANCHOR_PREFIX = ':IPA:'
SID_ANCHOR_PREFIX = ':SID:'
MIN_DOMAIN_LEVEL = 0
MAX_DOMAIN_LEVEL = 1

View File

@@ -1344,6 +1344,22 @@ class EmptyResult(NotFound):
errno = 4031
class InvalidDomainLevelError(ExecutionError):
"""
**4032** Raised when a operation could not be completed due to a invalid
domain level.
For example:
>>> raise InvalidDomainLevelError(reason='feature requires domain level 4')
Traceback (most recent call last):
...
InvalidDomainLevelError: feature requires domain level 4
"""
errno = 4032
class BuiltinError(ExecutionError):
"""
**4100** Base class for builtin execution errors (*4100 - 4199*).

View File

@@ -0,0 +1,138 @@
#
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
#
from collections import namedtuple
from ipalib import _
from ipalib import Command
from ipalib import errors
from ipalib import output
from ipalib.parameters import Int
from ipalib.plugable import Registry
from ipalib.plugins.baseldap import LDAPObject, LDAPUpdate, LDAPRetrieve
from ipapython.dn import DN
__doc__ = _("""
Raise the IPA Domain Level.
""")
register = Registry()
DomainLevelRange = namedtuple('DomainLevelRange', ['min', 'max'])
domainlevel_output = (
output.Output('result', int, _('Current domain level:')),
)
def get_domainlevel_dn(api):
domainlevel_dn = DN(
('cn', 'Domain Level'),
('cn', 'ipa'),
('cn', 'etc'),
api.env.basedn
)
return domainlevel_dn
def get_domainlevel_range(master_entry):
try:
return DomainLevelRange(
int(master_entry['ipaMinDomainLevel'][0]),
int(master_entry['ipaMaxDomainLevel'][0])
)
except KeyError:
return DomainLevelRange(0, 0)
def get_master_entries(ldap, api):
"""
Returns list of LDAPEntries representing IPA masters.
"""
container_masters = DN(
('cn', 'masters'),
('cn', 'ipa'),
('cn', 'etc'),
api.env.basedn
)
masters, _ = ldap.find_entries(
filter="(cn=*)",
base_dn=container_masters,
scope=ldap.SCOPE_ONELEVEL,
paged_search=True, # we need to make sure to get all of them
)
return masters
@register()
class domainlevel_get(Command):
__doc__ = _('Query current Domain Level.')
has_output = domainlevel_output
def execute(self, *args, **options):
ldap = self.api.Backend.ldap2
entry = ldap.get_entry(
get_domainlevel_dn(self.api),
['ipaDomainLevel']
)
return {'result': int(entry.single_value['ipaDomainLevel'])}
@register()
class domainlevel_set(Command):
__doc__ = _('Change current Domain Level.')
has_output = domainlevel_output
takes_args = (
Int('ipadomainlevel',
cli_name='level',
label=_('Domain Level'),
minvalue=0,
),
)
def execute(self, *args, **options):
"""
Checks all the IPA masters for supported domain level ranges.
If the desired domain level is within the supported range of all
masters, it will be raised.
Domain level cannot be lowered.
"""
ldap = self.api.Backend.ldap2
current_entry = ldap.get_entry(get_domainlevel_dn(self.api))
current_value = int(current_entry.single_value['ipadomainlevel'])
desired_value = int(args[0])
# Domain level cannot be lowered
if int(desired_value) < int(current_value):
message = _("Domain Level cannot be lowered.")
raise errors.InvalidDomainLevelError(message)
# Check if every master supports the desired level
for master in get_master_entries(ldap, self.api):
supported = get_domainlevel_range(master)
if supported.min > desired_value or supported.max < desired_value:
message = _("Domain Level cannot be raised to {0}, server {1} "
"does not support it."
.format(desired_value, master['cn'][0]))
raise errors.InvalidDomainLevelError(message)
current_entry.single_value['ipaDomainLevel'] = desired_value
ldap.update_entry(current_entry)
return {'result': int(current_entry.single_value['ipaDomainLevel'])}

View File

@@ -40,6 +40,7 @@ from ipaserver.install import upgradeinstance
from ipalib import api
from ipalib import certstore
from ipalib import errors
from ipalib import constants
from ipaplatform.tasks import tasks
from ipalib.constants import CACERT
from ipapython.dn import DN
@@ -62,6 +63,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
"70ipaotp.ldif",
"70topology.ldif",
"71idviews.ldif",
"72domainlevels.ldif",
"15rfc2307bis.ldif",
"15rfc4876.ldif")
@@ -186,7 +188,7 @@ info: IPA V2.0
class DsInstance(service.Service):
def __init__(self, realm_name=None, domain_name=None, dm_password=None,
fstore=None):
fstore=None, domainlevel=None):
service.Service.__init__(self, "dirsrv",
service_desc="directory server",
dm_password=dm_password,
@@ -209,6 +211,7 @@ class DsInstance(service.Service):
self.subject_base = None
self.open_ports = []
self.run_init_memberof = True
self.domainlevel = domainlevel
if realm_name:
self.suffix = ipautil.realm_to_suffix(self.realm)
self.__setup_sub_dict()
@@ -254,6 +257,7 @@ class DsInstance(service.Service):
def __common_post_setup(self):
self.step("initializing group membership", self.init_memberof)
self.step("adding master entry", self.__add_master_entry)
self.step("initializing domain level", self.__set_domain_level)
self.step("configuring Posix uid/gid generation",
self.__config_uidgid_gen)
self.step("adding replication acis", self.__add_replication_acis)
@@ -395,7 +399,10 @@ class DsInstance(service.Service):
IDMAX=self.idmax, HOST=self.fqdn,
ESCAPED_SUFFIX=str(self.suffix),
GROUP=DS_GROUP,
IDRANGE_SIZE=idrange_size
IDRANGE_SIZE=idrange_size,
DOMAIN_LEVEL=self.domainlevel,
MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL,
MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL,
)
def __create_instance(self):
@@ -1011,3 +1018,8 @@ class DsInstance(service.Service):
root_logger.debug('Unable to find certificate subject base in '
'certmap.conf')
return None
def __set_domain_level(self):
# Create global domain level entry and set the domain level
if self.domainlevel is not None:
self._ldap_mod("domainlevel.ldif", self.sub_dict)

View File

@@ -39,6 +39,7 @@ from ipaserver.install import installutils
from ipapython import ipautil, ipaldap
from ipalib import errors
from ipalib import api, create_api
from ipalib import constants
from ipaplatform.paths import paths
from ipaplatform import services
from ipapython.dn import DN
@@ -305,6 +306,10 @@ class LDAPUpdate:
self.sub_dict["TIME"] = int(time.time())
if not self.sub_dict.get("DOMAIN") and domain is not None:
self.sub_dict["DOMAIN"] = domain
if not self.sub_dict.get("MIN_DOMAIN_LEVEL"):
self.sub_dict["MIN_DOMAIN_LEVEL"] = str(constants.MIN_DOMAIN_LEVEL)
if not self.sub_dict.get("MAX_DOMAIN_LEVEL"):
self.sub_dict["MAX_DOMAIN_LEVEL"] = str(constants.MAX_DOMAIN_LEVEL)
self.api = create_api(mode=None)
self.api.bootstrap(in_server=True, context='updates')
self.api.finalize()

View File

@@ -338,7 +338,16 @@ NONOBJECT_PERMISSIONS = {
'serviceAuthenticationMethod', 'objectclassMap', 'attributeMap',
'profileTTL'
},
}
},
'System: Read Domain Level': {
'ipapermlocation': DN('cn=Domain Level,cn=ipa,cn=etc', api.env.basedn),
'ipapermtargetfilter': {'(objectclass=ipadomainlevelconfig)'},
'ipapermbindruletype': 'all',
'ipapermright': {'read', 'search', 'compare'},
'ipapermdefaultattr': {
'ipadomainlevel', 'objectclass',
},
},
}