trusts: support subdomains in a forest

Add IPA CLI to manage trust domains.

ipa trust-fetch-domains <trust>      -- fetch list of subdomains from AD side and add new ones to IPA
ipa trustdomain-find <trust>         -- show all available domains
ipa trustdomain-del <trust> <domain> -- remove domain from IPA view about <trust>
ipa trustdomain-enable <trust> <domain> -- allow users from trusted domain to access resources in IPA
ipa trustdomain-disable <trust> <domain> -- disable access to resources in IPA from trusted domain

By default all discovered trust domains are allowed to access IPA resources

IPA KDC needs also information for authentication paths to subdomains in case they
are not hierarchical under AD forest trust root. This information is managed via capaths
section in krb5.conf. SSSD should be able to generate it once
ticket https://fedorahosted.org/sssd/ticket/2093 is resolved.

part of https://fedorahosted.org/freeipa/ticket/3909
This commit is contained in:
Alexander Bokovoy 2013-09-18 17:04:19 +02:00 committed by Martin Kosek
parent 0637f590ed
commit 0b29bfde0d
3 changed files with 420 additions and 51 deletions

88
API.txt
View File

@ -3423,6 +3423,17 @@ option: Str('version?', exclude='webui')
output: Output('result', <type 'dict'>, None)
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: Output('value', <type 'unicode'>, None)
command: trust_fetch_domains
args: 1,4,4
arg: Str('cn', attribute=True, cli_name='realm', multivalue=False, primary_key=True, query=True, required=True)
option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
option: Flag('rights', autofill=True, default=False)
option: Str('version?', exclude='webui')
output: Output('count', <type 'int'>, None)
output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: Output('truncated', <type 'bool'>, None)
command: trust_find
args: 1,11,4
arg: Str('criteria?', noextrawhitespace=False)
@ -3497,6 +3508,83 @@ 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: Output('value', <type 'unicode'>, None)
command: trustdomain_add
args: 2,9,3
arg: Str('trustcn', cli_name='trust', query=True, required=True)
arg: Str('cn', attribute=True, cli_name='domain', multivalue=False, primary_key=True, required=True)
option: Str('addattr*', cli_name='addattr', exclude='webui')
option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
option: Str('ipantflatname', attribute=True, cli_name='flat_name', multivalue=False, required=False)
option: Str('ipanttrusteddomainsid', attribute=True, cli_name='sid', multivalue=False, required=False)
option: Str('ipanttrustpartner', attribute=True, cli_name='ipanttrustpartner', multivalue=False, required=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
option: Str('setattr*', cli_name='setattr', exclude='webui')
option: StrEnum('trust_type', autofill=True, cli_name='type', default=u'ad', values=(u'ad',))
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: Output('value', <type 'unicode'>, None)
command: trustdomain_del
args: 2,2,3
arg: Str('trustcn', cli_name='trust', query=True, required=True)
arg: Str('cn', attribute=True, cli_name='domain', multivalue=True, primary_key=True, query=True, required=True)
option: Flag('continue', autofill=True, cli_name='continue', default=False)
option: Str('version?', exclude='webui')
output: Output('result', <type 'dict'>, None)
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: Output('value', <type 'unicode'>, None)
command: trustdomain_disable
args: 2,1,3
arg: Str('trustcn', cli_name='trust', query=True, required=True)
arg: Str('cn', attribute=True, cli_name='domain', multivalue=False, primary_key=True, query=True, required=True)
option: Str('version?', exclude='webui')
output: Output('result', <type 'bool'>, None)
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: Output('value', <type 'unicode'>, None)
command: trustdomain_enable
args: 2,1,3
arg: Str('trustcn', cli_name='trust', query=True, required=True)
arg: Str('cn', attribute=True, cli_name='domain', multivalue=False, primary_key=True, query=True, required=True)
option: Str('version?', exclude='webui')
output: Output('result', <type 'bool'>, None)
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: Output('value', <type 'unicode'>, None)
command: trustdomain_find
args: 2,10,4
arg: Str('trustcn', cli_name='trust', query=True, required=True)
arg: Str('criteria?', noextrawhitespace=False)
option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
option: Str('cn', attribute=True, autofill=False, cli_name='domain', multivalue=False, primary_key=True, query=True, required=False)
option: Str('ipantflatname', attribute=True, autofill=False, cli_name='flat_name', multivalue=False, query=True, required=False)
option: Str('ipanttrusteddomainsid', attribute=True, autofill=False, cli_name='sid', multivalue=False, query=True, required=False)
option: Str('ipanttrustpartner', attribute=True, autofill=False, cli_name='ipanttrustpartner', multivalue=False, query=True, required=False)
option: Flag('pkey_only?', autofill=True, default=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
option: Int('sizelimit?', autofill=False, minvalue=0)
option: Int('timelimit?', autofill=False, minvalue=0)
option: Str('version?', exclude='webui')
output: Output('count', <type 'int'>, None)
output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
output: Output('truncated', <type 'bool'>, None)
command: trustdomain_mod
args: 2,11,3
arg: Str('trustcn', cli_name='trust', query=True, required=True)
arg: Str('cn', attribute=True, cli_name='domain', multivalue=False, primary_key=True, query=True, required=True)
option: Str('addattr*', cli_name='addattr', exclude='webui')
option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
option: Str('delattr*', cli_name='delattr', exclude='webui')
option: Str('ipantflatname', attribute=True, autofill=False, cli_name='flat_name', multivalue=False, required=False)
option: Str('ipanttrusteddomainsid', attribute=True, autofill=False, cli_name='sid', multivalue=False, required=False)
option: Str('ipanttrustpartner', attribute=True, autofill=False, cli_name='ipanttrustpartner', multivalue=False, required=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
option: Flag('rights', autofill=True, default=False)
option: Str('setattr*', cli_name='setattr', exclude='webui')
option: StrEnum('trust_type', autofill=True, cli_name='type', default=u'ad', values=(u'ad',))
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: Output('value', <type 'unicode'>, None)
command: user_add
args: 1,35,3
arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)

View File

@ -181,6 +181,13 @@ def trust_status_string(level):
string = _trust_status_dict.get(level, _trust_type_dict_unknown)
return unicode(string)
def make_trust_dn(env, trust_type, dn):
assert isinstance(dn, DN)
if trust_type:
container_dn = DN(('cn', trust_type), env.container_trusts, env.basedn)
return DN(dn, container_dn)
return dn
class trust(LDAPObject):
"""
Trust object.
@ -195,7 +202,8 @@ class trust(LDAPObject):
'ipantauthtrustoutgoing', 'ipanttrustauthincoming', 'ipanttrustforesttrustinfo',
'ipanttrustposixoffset', 'ipantsupportedencryptiontypes' ]
search_display_attributes = ['cn', 'ipantflatname',
'ipanttrusteddomainsid', 'ipanttrusttype' ]
'ipanttrusteddomainsid', 'ipanttrusttype',
'ipantsidblacklistincoming', 'ipantsidblacklistoutgoing' ]
label = _('Trusts')
label_singular = _('Trust')
@ -241,12 +249,26 @@ class trust(LDAPObject):
raise errors.ValidationError(name=attr,
error=_("invalid SID: %(value)s") % dict(value=value))
def make_trust_dn(env, trust_type, dn):
assert isinstance(dn, DN)
if trust_type in trust.trust_types:
container_dn = DN(('cn', trust_type), env.container_trusts, env.basedn)
return DN(dn[0], container_dn)
return dn
def get_dn(self, *keys, **kwargs):
sdn = map(lambda x: ('cn', x), keys)
sdn.reverse()
trust_type = kwargs.get('trust_type')
if trust_type is None:
ldap = self.backend
filter = ldap.make_filter({'objectclass': ['ipaNTTrustedDomain'], 'cn': [keys[-1]]})
filter = ldap.combine_filters((filter, "ipaNTSIDBlacklistIncoming=*"), rules=ldap.MATCH_ALL)
try:
result = ldap.get_entries(DN(self.container_dn, self.env.basedn),
ldap.SCOPE_SUBTREE, filter, [''])
except errors.NotFound:
trust_type = u'ad'
else:
if len(result) > 1:
raise errors.OnlyOneValueAllowed(attr='trust domain')
return result[0].dn
dn=make_trust_dn(self.env, trust_type, DN(*sdn))
return dn
class trust_add(LDAPCreate):
__doc__ = _('''
@ -589,10 +611,13 @@ sides.
def execute_ad(self, full_join, *keys, **options):
# Join domain using full credentials and with random trustdom
# secret (will be generated by the join method)
try:
api.Command['trust_show'](keys[-1])
# First see if the trust is already in place
# Force retrieval of the trust object by not passing trust_type
dn = self.obj.get_dn(keys[-1])
if dn:
summary = _('Re-established trust to domain "%(value)s"')
except errors.NotFound:
else:
summary = self.msg_summary
# 1. Full access to the remote domain. Use admin credentials and
@ -665,14 +690,6 @@ class trust_del(LDAPDelete):
msg_summary = _('Deleted trust "%(value)s"')
def pre_callback(self, ldap, dn, *keys, **options):
assert isinstance(dn, DN)
try:
result = self.api.Command.trust_show(keys[-1])
except errors.NotFound, e:
self.obj.handle_not_found(*keys)
return result['result']['dn']
class trust_mod(LDAPUpdate):
__doc__ = _("""
Modify a trust (for future use).
@ -686,16 +703,10 @@ class trust_mod(LDAPUpdate):
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
assert isinstance(dn, DN)
result = None
try:
result = self.api.Command.trust_show(keys[-1])
except errors.NotFound, e:
self.obj.handle_not_found(*keys)
self.obj.validate_sid_blacklists(entry_attrs)
# TODO: we found the trust object, now modify it
return result['result']['dn']
return dn
class trust_find(LDAPSearch):
__doc__ = _('Search for trusts.')
@ -709,8 +720,10 @@ class trust_find(LDAPSearch):
# Since all trusts types are stored within separate containers under 'cn=trusts',
# search needs to be done on a sub-tree scope
def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
assert isinstance(base_dn, DN)
return (filters, base_dn, ldap.SCOPE_SUBTREE)
# list only trust, not trust domains
trust_filter = '(ipaNTSIDBlacklistIncoming=*)'
filter = ldap.combine_filters((filters, trust_filter), rules=ldap.MATCH_ALL)
return (filter, base_dn, ldap.SCOPE_SUBTREE)
def post_callback(self, ldap, entries, truncated, *args, **options):
if options.get('pkey_only', False):
@ -731,30 +744,6 @@ class trust_show(LDAPRetrieve):
has_output_params = LDAPRetrieve.has_output_params + trust_output_params +\
(Str('ipanttrusttype'), Str('ipanttrustdirection'))
def execute(self, *keys, **options):
error = None
result = None
for trust_type in trust.trust_types:
options['trust_show_type'] = trust_type
try:
result = super(trust_show, self).execute(*keys, **options)
except errors.NotFound, e:
result = None
error = e
if result:
break
if error or not result:
self.obj.handle_not_found(*keys)
return result
def pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
assert isinstance(dn, DN)
if 'trust_show_type' in options:
return make_trust_dn(self.env, options['trust_show_type'], dn)
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
# Translate ipanttrusttype to trusttype
@ -1091,3 +1080,241 @@ class sidgen_was_run(Command):
return dict(result=True)
api.register(sidgen_was_run)
class trustdomain(LDAPObject):
"""
Object representing a domain of the AD trust.
"""
parent_object = 'trust'
trust_type_idx = {'2':u'ad'}
object_name = _('trust domain')
object_name_plural = _('trust domains')
object_class = ['ipaNTTrustedDomain']
default_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', 'ipanttrustpartner']
search_display_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', ]
label = _('Trusted domains')
label_singular = _('Trusted domain')
takes_params = (
Str('cn',
label=_('Domain name'),
cli_name='domain',
primary_key=True
),
Str('ipantflatname?',
cli_name='flat_name',
label=_('Domain NetBIOS name'),
),
Str('ipanttrusteddomainsid?',
cli_name='sid',
label=_('Domain Security Identifier'),
),
Str('ipanttrustpartner?',
label=_('Trusted domain partner'),
flags=['no_display', 'no_option'],
),
)
# LDAPObject.get_dn() only passes all but last element of keys and no kwargs
# to the parent object's get_dn() no matter what you pass to it. Make own get_dn()
# as we really need all elements to construct proper dn.
def get_dn(self, *keys, **kwargs):
sdn = map(lambda x: ('cn', x), keys)
sdn.reverse()
trust_type = kwargs.get('trust_type')
if not trust_type:
trust_type=u'ad'
dn=make_trust_dn(self.env, trust_type, DN(*sdn))
return dn
api.register(trustdomain)
class trustdomain_find(LDAPSearch):
__doc__ = _('Search domains of the trust')
def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
return (filters, base_dn, ldap.SCOPE_SUBTREE)
api.register(trustdomain_find)
class trustdomain_mod(LDAPUpdate):
__doc__ = _('Modify trustdomain of the trust')
NO_CLI = True
takes_options = LDAPUpdate.takes_options + (_trust_type_option,)
api.register(trustdomain_mod)
class trustdomain_add(LDAPCreate):
__doc__ = _('Allow access from the trusted domain')
NO_CLI = True
takes_options = LDAPCreate.takes_options + (_trust_type_option,)
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
if 'ipanttrustpartner' in options:
entry_attrs['ipanttrustpartner'] = [options['ipanttrustpartner']]
return dn
api.register(trustdomain_add)
class trustdomain_del(LDAPDelete):
__doc__ = _('Remove infromation about the domain associated with the trust.')
msg_summary = _('Removed information about the trusted domain "%(value)s"')
def execute(self, *keys, **options):
# Note that pre-/post- callback handling for LDAPDelete is causing pre_callback
# to always receive empty keys. We need to catch the case when root domain is being deleted
for domain in keys[1]:
if keys[0].lower() == domain:
raise errors.ValidationError(name='domain',
error=_("cannot delete root domain of the trust, use trust-del to delete the trust itself"))
try:
res = api.Command.trustdomain_enable(keys[0], domain)
except errors.AlreadyActive:
pass
result = super(trustdomain_del, self).execute(*keys, **options)
result['value'] = u','.join(keys[1])
return result
api.register(trustdomain_del)
def fetch_domains_from_trust(self, trustinstance, trust_entry):
trust_name = trust_entry['cn'][0]
domains = ipaserver.dcerpc.fetch_domains(self.api, trustinstance.local_flatname, trust_name)
result = []
if not domains:
return None
for dom in domains:
dom['trust_type'] = u'ad'
try:
name = dom['cn']
del dom['cn']
if 'all' in options:
dom['all'] = options['all']
if 'raw' in options:
dom['raw'] = options['raw']
res = self.api.Command.trustdomain_add(trust_name, name, **dom)
result.append(res['result'])
except errors.DuplicateEntry:
# Ignore updating duplicate entries
pass
return result
class trust_fetch_domains(LDAPRetrieve):
__doc__ = _('Refresh list of the domains associated with the trust')
has_output = output.standard_list_of_entries
def execute(self, *keys, **options):
if not _bindings_installed:
raise errors.NotFound(
name=_('AD Trust setup'),
reason=_(
'Cannot perform join operation without Samba 4 support '
'installed. Make sure you have installed server-trust-ad '
'sub-package of IPA'
)
)
trust = self.api.Command.trust_show(keys[0], raw=True)['result']
trustinstance = ipaserver.dcerpc.TrustDomainJoins(self.api)
if not trustinstance.configured:
raise errors.NotFound(
name=_('AD Trust setup'),
reason=_(
'Cannot perform join operation without own domain '
'configured. Make sure you have run ipa-adtrust-install '
'on the IPA server first'
)
)
domains = fetch_domains_from_trust(self, trustinstance, trust)
result = dict()
if len(domains) > 0:
result['summary'] = unicode(_('List of trust domains successfully refreshed'))
else:
result['summary'] = unicode(_('No new trust domains were found'))
result['result'] = domains
result['count'] = len(domains)
result['truncated'] = False
return result
api.register(trust_fetch_domains)
class trustdomain_enable(LDAPQuery):
__doc__ = _('Allow use of IPA resources by the domain of the trust')
has_output = output.standard_value
msg_summary = _('Enabled trust domain "%(value)s"')
def execute(self, *keys, **options):
ldap = self.api.Backend.ldap2
if keys[0].lower() == keys[1].lower():
raise errors.ValidationError(name='domain',
error=_("Root domain of the trust is always enabled for the existing trust"))
try:
trust_dn = self.obj.get_dn(keys[0], trust_type=u'ad')
trust_entry = ldap.get_entry(trust_dn)
except errors.NotFound:
self.api.Object[self.obj.parent_object].handle_not_found(keys[0])
dn = self.obj.get_dn(keys[0], keys[1], trust_type=u'ad')
try:
entry = ldap.get_entry(dn)
sid = entry['ipanttrusteddomainsid'][0]
if sid in trust_entry['ipantsidblacklistincoming']:
trust_entry['ipantsidblacklistincoming'].remove(sid)
ldap.update_entry(trust_entry)
else:
raise errors.AlreadyActive()
except errors.NotFound:
self.obj.handle_not_found(*keys)
return dict(
result=True,
value=keys[1],
)
api.register(trustdomain_enable)
class trustdomain_disable(LDAPQuery):
__doc__ = _('Disable use of IPA resources by the domain of the trust')
has_output = output.standard_value
msg_summary = _('Disabled trust domain "%(value)s"')
def execute(self, *keys, **options):
ldap = self.api.Backend.ldap2
if keys[0].lower() == keys[1].lower():
raise errors.ValidationError(name='domain',
error=_("cannot disable root domain of the trust, use trust-del to delete the trust itself"))
try:
trust_dn = self.obj.get_dn(keys[0], trust_type=u'ad')
trust_entry = ldap.get_entry(trust_dn)
except errors.NotFound:
self.api.Object[self.obj.parent_object].handle_not_found(keys[0])
dn = self.obj.get_dn(keys[0], keys[1], trust_type=u'ad')
try:
entry = ldap.get_entry(dn)
sid = entry['ipanttrusteddomainsid'][0]
if not (sid in trust_entry['ipantsidblacklistincoming']):
trust_entry['ipantsidblacklistincoming'].append(sid)
ldap.update_entry(trust_entry)
else:
raise errors.AlreadyInactive()
except errors.NotFound:
self.obj.handle_not_found(*keys)
return dict(
result=True,
value=keys[1],
)
api.register(trustdomain_disable)

View File

@ -1002,6 +1002,60 @@ class TrustDomainInstance(object):
return True
return False
def fetch_domains(api, mydomain, trustdomain):
trust_flags = dict(
NETR_TRUST_FLAG_IN_FOREST = 0x00000001,
NETR_TRUST_FLAG_OUTBOUND = 0x00000002,
NETR_TRUST_FLAG_TREEROOT = 0x00000004,
NETR_TRUST_FLAG_PRIMARY = 0x00000008,
NETR_TRUST_FLAG_NATIVE = 0x00000010,
NETR_TRUST_FLAG_INBOUND = 0x00000020,
NETR_TRUST_FLAG_MIT_KRB5 = 0x00000080,
NETR_TRUST_FLAG_AES = 0x00000100)
trust_attributes = dict(
NETR_TRUST_ATTRIBUTE_NON_TRANSITIVE = 0x00000001,
NETR_TRUST_ATTRIBUTE_UPLEVEL_ONLY = 0x00000002,
NETR_TRUST_ATTRIBUTE_QUARANTINED_DOMAIN = 0x00000004,
NETR_TRUST_ATTRIBUTE_FOREST_TRANSITIVE = 0x00000008,
NETR_TRUST_ATTRIBUTE_CROSS_ORGANIZATION = 0x00000010,
NETR_TRUST_ATTRIBUTE_WITHIN_FOREST = 0x00000020,
NETR_TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL = 0x00000040)
domval = DomainValidator(api)
(ccache_name, principal) = domval.kinit_as_http(trustdomain)
if ccache_name:
with installutils.private_ccache(path=ccache_name):
td = TrustDomainInstance('')
td.parm.set('workgroup', mydomain)
td.creds = credentials.Credentials()
td.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
td.creds.guess(td.parm)
netrc = net.Net(creds=td.creds, lp=td.parm)
try:
result = netrc.finddc(domain=trustdomain, flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS)
except RuntimeError, e:
raise assess_dcerpc_exception(message=str(e))
if not result:
return None
td.retrieve(unicode(result.pdc_dns_name))
netr_pipe = netlogon.netlogon(td.binding, td.parm, td.creds)
domains = netr_pipe.netr_DsrEnumerateDomainTrusts(td.binding, 1)
result = []
for t in domains.array:
if ((t.trust_attributes & trust_attributes['NETR_TRUST_ATTRIBUTE_WITHIN_FOREST']) and
(t.trust_flags & trust_flags['NETR_TRUST_FLAG_IN_FOREST'])):
res = dict()
res['cn'] = unicode(t.dns_name)
res['ipantflatname'] = unicode(t.netbios_name)
res['ipanttrusteddomainsid'] = unicode(t.sid)
res['ipanttrustpartner'] = res['cn']
result.append(res)
return result
class TrustDomainJoins(object):
def __init__(self, api):
self.api = api