From c2af032c0333f7e210c54369159d1d9f5e3fec74 Mon Sep 17 00:00:00 2001 From: Martin Babinsky Date: Thu, 23 Jun 2016 18:54:49 +0200 Subject: Migrate management framework plugins to use Principal parameter All plugins will now use this parameter and common code for all operations on Kerberos principals. Additional semantic validators and normalizers were added to determine or append a correct realm so that the previous behavior is kept intact. https://fedorahosted.org/freeipa/ticket/3864 Reviewed-By: David Kupka Reviewed-By: Jan Cholasta --- API.txt | 76 +++++++++---------- ipaserver/plugins/baseuser.py | 57 ++++----------- ipaserver/plugins/caacl.py | 17 ++--- ipaserver/plugins/cert.py | 88 ++++++++++------------ ipaserver/plugins/host.py | 25 +++++-- ipaserver/plugins/passwd.py | 21 ++++-- ipaserver/plugins/service.py | 101 ++++++++++---------------- ipaserver/plugins/vault.py | 46 ++++++------ ipatests/test_xmlrpc/test_host_plugin.py | 1 + ipatests/test_xmlrpc/test_service_plugin.py | 9 ++- ipatests/test_xmlrpc/test_stageuser_plugin.py | 6 +- ipatests/test_xmlrpc/test_user_plugin.py | 5 +- 12 files changed, 213 insertions(+), 239 deletions(-) diff --git a/API.txt b/API.txt index c01692e17..704dcde3c 100644 --- a/API.txt +++ b/API.txt @@ -738,14 +738,14 @@ option: Int('max_serial_number?', autofill=False) option: Int('min_serial_number?', autofill=False) option: Str('no_host*', cli_name='no_hosts') option: Flag('no_members', autofill=True, default=True) -option: Str('no_service*', cli_name='no_services') +option: Principal('no_service*', cli_name='no_services') option: Str('no_user*', cli_name='no_users') option: Flag('pkey_only?', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Int('revocation_reason?', autofill=False) option: DateTime('revokedon_from?', autofill=False) option: DateTime('revokedon_to?', autofill=False) -option: Str('service*', cli_name='services') +option: Principal('service*', cli_name='services') option: Int('sizelimit?') option: Str('subject?', autofill=False) option: Int('timelimit?') @@ -771,7 +771,7 @@ arg: Str('csr', cli_name='csr_file') option: Flag('add', autofill=True, default=False) option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa') -option: Str('principal') +option: Principal('principal') option: Str('profile_id?') option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Str('request_type', autofill=True, default=u'pkcs10') @@ -2436,7 +2436,7 @@ option: Bool('ipakrbokasdelegate?', autofill=False, cli_name='ok_as_delegate') option: Bool('ipakrbrequirespreauth?', autofill=False, cli_name='requires_pre_auth') option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') option: Str('krbprincipalauthind*', autofill=False, cli_name='auth_ind') -option: Str('krbprincipalname?', cli_name='principalname') +option: Principal('krbprincipalname?', cli_name='principalname') option: Str('l?', autofill=False, cli_name='locality') option: Str('macaddress*', autofill=False) option: Flag('no_members', autofill=True, default=False) @@ -3401,7 +3401,7 @@ output: Output('summary', type=[, ]) output: PrimaryKey('value') command: passwd/1 args: 3,2,3 -arg: Str('principal', autofill=True, cli_name='user') +arg: Principal('principal', autofill=True, cli_name='user') arg: Password('password') arg: Password('current_password', autofill=True, confirm=False) option: Password('otp?', confirm=False) @@ -4271,7 +4271,7 @@ output: Output('summary', type=[, ]) output: PrimaryKey('value') command: service_add/1 args: 1,12,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('force', autofill=True, default=False) @@ -4289,7 +4289,7 @@ output: Output('summary', type=[, ]) output: PrimaryKey('value') command: service_add_cert/1 args: 1,5,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) @@ -4300,7 +4300,7 @@ output: Output('summary', type=[, ]) output: PrimaryKey('value') command: service_add_host/1 args: 1,5,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('host*', alwaysask=True, cli_name='hosts') option: Flag('no_members', autofill=True, default=False) @@ -4311,7 +4311,7 @@ output: Output('failed', type=[]) output: Entry('result') command: service_allow_create_keytab/1 args: 1,8,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Str('host*', alwaysask=True, cli_name='hosts') @@ -4325,7 +4325,7 @@ output: Output('failed', type=[]) output: Entry('result') command: service_allow_retrieve_keytab/1 args: 1,8,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Str('host*', alwaysask=True, cli_name='hosts') @@ -4339,7 +4339,7 @@ output: Output('failed', type=[]) output: Entry('result') command: service_del/1 args: 1,2,3 -arg: Str('krbprincipalname+', cli_name='principal') +arg: Principal('krbprincipalname+', cli_name='principal') option: Flag('continue', autofill=True, cli_name='continue', default=False) option: Str('version?') output: Output('result', type=[]) @@ -4347,14 +4347,14 @@ output: Output('summary', type=[, ]) output: ListOfPrimaryKeys('value') command: service_disable/1 args: 1,1,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Str('version?') output: Output('result', type=[]) output: Output('summary', type=[, ]) output: PrimaryKey('value') command: service_disallow_create_keytab/1 args: 1,8,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Str('host*', alwaysask=True, cli_name='hosts') @@ -4368,7 +4368,7 @@ output: Output('failed', type=[]) output: Entry('result') command: service_disallow_retrieve_keytab/1 args: 1,8,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Str('host*', alwaysask=True, cli_name='hosts') @@ -4386,7 +4386,7 @@ arg: Str('criteria?') option: Flag('all', autofill=True, cli_name='all', default=False) option: StrEnum('ipakrbauthzdata*', autofill=False, cli_name='pac_type', values=[u'MS-PAC', u'PAD', u'NONE']) option: Str('krbprincipalauthind*', autofill=False, cli_name='auth_ind') -option: Str('krbprincipalname?', autofill=False, cli_name='principal') +option: Principal('krbprincipalname?', autofill=False, cli_name='principal') option: Str('man_by_host*', cli_name='man_by_hosts') option: Flag('no_members', autofill=True, default=True) option: Str('not_man_by_host*', cli_name='not_man_by_hosts') @@ -4401,7 +4401,7 @@ output: Output('summary', type=[, ]) output: Output('truncated', type=[]) command: service_mod/1 args: 1,13,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Str('addattr*', cli_name='addattr') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('delattr*', cli_name='delattr') @@ -4420,7 +4420,7 @@ output: Output('summary', type=[, ]) output: PrimaryKey('value') command: service_remove_cert/1 args: 1,5,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) @@ -4431,7 +4431,7 @@ output: Output('summary', type=[, ]) output: PrimaryKey('value') command: service_remove_host/1 args: 1,5,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('host*', alwaysask=True, cli_name='hosts') option: Flag('no_members', autofill=True, default=False) @@ -4442,7 +4442,7 @@ output: Output('failed', type=[]) output: Entry('result') command: service_show/1 args: 1,6,3 -arg: Str('krbprincipalname', cli_name='principal') +arg: Principal('krbprincipalname', cli_name='principal') option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('no_members', autofill=True, default=False) option: Str('out?') @@ -4646,7 +4646,7 @@ option: Str('ipatokenradiusconfiglink?', cli_name='radius') option: Str('ipatokenradiususername?', cli_name='radius_username') option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp']) option: DateTime('krbprincipalexpiration?', cli_name='principal_expiration') -option: Str('krbprincipalname?', autofill=True, cli_name='principal') +option: Principal('krbprincipalname?', autofill=True, cli_name='principal') option: Str('l?', cli_name='city') option: Str('loginshell?', cli_name='shell') option: Str('mail*', cli_name='email') @@ -4717,7 +4717,7 @@ option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp']) option: DateTime('krbprincipalexpiration?', autofill=False, cli_name='principal_expiration') -option: Str('krbprincipalname?', autofill=False, cli_name='principal') +option: Principal('krbprincipalname?', autofill=False, cli_name='principal') option: Str('l?', autofill=False, cli_name='city') option: Str('loginshell?', autofill=False, cli_name='shell') option: Str('mail*', autofill=False, cli_name='email') @@ -5633,7 +5633,7 @@ option: Str('ipatokenradiusconfiglink?', cli_name='radius') option: Str('ipatokenradiususername?', cli_name='radius_username') option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp']) option: DateTime('krbprincipalexpiration?', cli_name='principal_expiration') -option: Str('krbprincipalname?', autofill=True, cli_name='principal') +option: Principal('krbprincipalname?', autofill=True, cli_name='principal') option: Str('l?', cli_name='city') option: Str('loginshell?', cli_name='shell') option: Str('mail*', cli_name='email') @@ -5732,7 +5732,7 @@ option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp']) option: DateTime('krbprincipalexpiration?', autofill=False, cli_name='principal_expiration') -option: Str('krbprincipalname?', autofill=False, cli_name='principal') +option: Principal('krbprincipalname?', autofill=False, cli_name='principal') option: Str('l?', autofill=False, cli_name='city') option: Str('loginshell?', autofill=False, cli_name='shell') option: Str('mail*', autofill=False, cli_name='email') @@ -5899,7 +5899,7 @@ option: Bytes('ipavaultsalt?', cli_name='salt') option: StrEnum('ipavaulttype?', autofill=True, cli_name='type', default=u'symmetric', values=[u'standard', u'symmetric', u'asymmetric']) option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('setattr*', cli_name='setattr') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') @@ -5914,7 +5914,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('services*', alwaysask=True, cli_name='services') option: Flag('shared?', autofill=True, default=False) option: Str('user*', alwaysask=True, cli_name='users') @@ -5930,7 +5930,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('services*', alwaysask=True, cli_name='services') option: Flag('shared?', autofill=True, default=False) option: Str('user*', alwaysask=True, cli_name='users') @@ -5945,7 +5945,7 @@ arg: Str('cn', cli_name='name') option: Flag('all', autofill=True, cli_name='all', default=False) option: Bytes('nonce') option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Bytes('session_key') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') @@ -5958,7 +5958,7 @@ command: vault_del/1 args: 1,5,3 arg: Str('cn+', cli_name='name') option: Flag('continue', autofill=True, cli_name='continue', default=False) -option: Str('service?') +option: Principal('service?') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') option: Str('version?') @@ -5975,7 +5975,7 @@ option: StrEnum('ipavaulttype?', autofill=False, cli_name='type', default=u'symm option: Flag('no_members', autofill=True, default=True) option: Flag('pkey_only?', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Flag('services?', autofill=True, default=False) option: Flag('shared?', autofill=True, default=False) option: Int('sizelimit?', autofill=False) @@ -6000,7 +6000,7 @@ option: StrEnum('ipavaulttype?', autofill=False, cli_name='type', default=u'symm option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Flag('rights', autofill=True, default=False) -option: Str('service?') +option: Principal('service?') option: Str('setattr*', cli_name='setattr') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') @@ -6015,7 +6015,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('services*', alwaysask=True, cli_name='services') option: Flag('shared?', autofill=True, default=False) option: Str('user*', alwaysask=True, cli_name='users') @@ -6031,7 +6031,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('services*', alwaysask=True, cli_name='services') option: Flag('shared?', autofill=True, default=False) option: Str('user*', alwaysask=True, cli_name='users') @@ -6045,7 +6045,7 @@ args: 1,7,3 arg: Str('cn', cli_name='name') option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Bytes('session_key') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') @@ -6060,7 +6060,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Flag('rights', autofill=True, default=False) -option: Str('service?') +option: Principal('service?') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') option: Str('version?') @@ -6082,7 +6082,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('services*', alwaysask=True, cli_name='services') option: Flag('shared?', autofill=True, default=False) option: Str('user*', alwaysask=True, cli_name='users') @@ -6094,7 +6094,7 @@ output: Entry('result') command: vaultcontainer_del/1 args: 0,5,3 option: Flag('continue', autofill=True, cli_name='continue', default=False) -option: Str('service?') +option: Principal('service?') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') option: Str('version?') @@ -6107,7 +6107,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('group*', alwaysask=True, cli_name='groups') option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) -option: Str('service?') +option: Principal('service?') option: Str('services*', alwaysask=True, cli_name='services') option: Flag('shared?', autofill=True, default=False) option: Str('user*', alwaysask=True, cli_name='users') @@ -6122,7 +6122,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Flag('no_members', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False) option: Flag('rights', autofill=True, default=False) -option: Str('service?') +option: Principal('service?') option: Flag('shared?', autofill=True, default=False) option: Str('username?', cli_name='user') option: Str('version?') diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py index 9c4af66f9..cbb04aaad 100644 --- a/ipaserver/plugins/baseuser.py +++ b/ipaserver/plugins/baseuser.py @@ -23,13 +23,16 @@ import six from ipalib import api, errors from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes +from ipalib.parameters import Principal from ipalib.plugable import Registry from .baseldap import ( DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember) -from .service import validate_certificate +from ipaserver.plugins.service import ( + validate_certificate, validate_realm, normalize_principal) from ipalib.request import context from ipalib import _ +from ipapython import kerberos from ipapython.ipautil import ipa_generate_password from ipapython.ipavalidate import Email from ipalib.util import ( @@ -93,45 +96,14 @@ def convert_nsaccountlock(entry_attrs): nsaccountlock = Bool('temp') entry_attrs['nsaccountlock'] = nsaccountlock.convert(entry_attrs['nsaccountlock'][0]) -def split_principal(principal): - """ - Split the principal into its components and do some basic validation. - - Automatically append our realm if it wasn't provided. - """ - realm = None - parts = principal.split('@') - user = parts[0].lower() - if len(parts) > 2: - raise errors.MalformedUserPrincipal(principal=principal) - - if len(parts) == 2: - realm = parts[1].upper() - # At some point we'll support multiple realms - if realm != api.env.realm: - raise errors.RealmMismatch() - else: - realm = api.env.realm - - return (user, realm) -def validate_principal(ugettext, principal): - """ - All the real work is done in split_principal. - """ - (user, realm) = split_principal(principal) - return None - -def normalize_principal(principal): - """ - Ensure that the name in the principal is lower-case. The realm is - upper-case by convention but it isn't required. - - The principal is validated at this point. - """ - (user, realm) = split_principal(principal) - return unicode('%s@%s' % (user, realm)) +def normalize_user_principal(value): + principal = kerberos.Principal(normalize_principal(value)) + lowercase_components = ((principal.username.lower(),) + + principal.components[1:]) + return unicode( + kerberos.Principal(lowercase_components, realm=principal.realm)) def fix_addressbook_permission_bindrule(name, template, is_new, @@ -239,13 +211,16 @@ class baseuser(LDAPObject): cli_name='shell', label=_('Login shell'), ), - Str('krbprincipalname?', validate_principal, + Principal( + 'krbprincipalname?', + validate_realm, cli_name='principal', label=_('Kerberos principal'), - default_from=lambda uid: '%s@%s' % (uid.lower(), api.env.realm), + default_from=lambda uid: kerberos.Principal.from_text( + uid.lower(), realm=api.env.realm), autofill=True, flags=['no_update'], - normalizer=lambda value: normalize_principal(value), + normalizer=normalize_user_principal, ), DateTime('krbprincipalexpiration?', cli_name='principal_expiration', diff --git a/ipaserver/plugins/caacl.py b/ipaserver/plugins/caacl.py index a543a1de7..3f813a7ef 100644 --- a/ipaserver/plugins/caacl.py +++ b/ipaserver/plugins/caacl.py @@ -3,6 +3,7 @@ # import pyhbac +import six from ipalib import api, errors, output from ipalib import Bool, Str, StrEnum @@ -13,10 +14,11 @@ from .baseldap import ( LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember, global_output_params, pkey_to_value) from .hbacrule import is_all -from .service import normalize_principal, split_any_principal from ipalib import _, ngettext from ipapython.dn import DN +if six.PY3: + unicode = str __doc__ = _(""" Manage CA ACL rules. @@ -58,24 +60,21 @@ register = Registry() def _acl_make_request(principal_type, principal, ca_id, profile_id): """Construct HBAC request for the given principal, CA and profile""" - service, name, realm = split_any_principal(principal) req = pyhbac.HbacRequest() req.targethost.name = ca_id req.service.name = profile_id - if principal_type == 'user': - req.user.name = name - elif principal_type == 'host': - req.user.name = name + if principal_type == 'user' or principal_type == 'host': + req.user.name = principal.username elif principal_type == 'service': - req.user.name = normalize_principal(principal) + req.user.name = unicode(principal) groups = [] if principal_type == 'user': - user_obj = api.Command.user_show(name)['result'] + user_obj = api.Command.user_show(principal.username)['result'] groups = user_obj.get('memberof_group', []) groups += user_obj.get('memberofindirect_group', []) elif principal_type == 'host': - host_obj = api.Command.host_show(name)['result'] + host_obj = api.Command.host_show(principal.hostname)['result'] groups = host_obj.get('memberof_hostgroup', []) groups += host_obj.get('memberofindirect_hostgroup', []) req.user.groups = sorted(set(groups)) diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py index 4cd2ab096..2f7904cd7 100644 --- a/ipaserver/plugins/cert.py +++ b/ipaserver/plugins/cert.py @@ -38,18 +38,18 @@ from ipalib import ngettext from ipalib.constants import IPA_CA_CN from ipalib.crud import Create, PKQuery, Retrieve, Search from ipalib.frontend import Method, Object -from ipalib.parameters import Bytes, DateTime, DNParam +from ipalib.parameters import Bytes, DateTime, DNParam, Principal from ipalib.plugable import Registry from .virtual import VirtualCommand from .baseldap import pkey_to_value -from .service import split_any_principal from .certprofile import validate_profile_id from .caacl import acl_evaluate from ipalib.text import _ from ipalib.request import context from ipalib import output -from .service import validate_principal +from ipapython import kerberos from ipapython.dn import DN +from ipaserver.plugins.service import normalize_principal, validate_realm if six.PY3: unicode = str @@ -216,35 +216,21 @@ def normalize_serial_number(num): return unicode(num) -def get_host_from_principal(principal): - """ - Given a principal with or without a realm return the - host portion. - """ - validate_principal(None, principal) - realm = principal.find('@') - slash = principal.find('/') - if realm == -1: - realm = len(principal) - hostname = principal[slash+1:realm] - - return hostname - def ca_enabled_check(): if not api.Command.ca_is_enabled()['result']: raise errors.NotFound(reason=_('CA is not configured')) -def caacl_check(principal_type, principal_string, ca, profile_id): +def caacl_check(principal_type, principal, ca, profile_id): principal_type_map = {USER: 'user', HOST: 'host', SERVICE: 'service'} if not acl_evaluate( principal_type_map[principal_type], - principal_string, ca, profile_id): + principal, ca, profile_id): raise errors.ACIError(info=_( "Principal '%(principal)s' " "is not permitted to use CA '%(ca)s' " "with profile '%(profile_id)s' for certificate issuance." ) % dict( - principal=principal_string, + principal=unicode(principal), ca=ca, profile_id=profile_id ) @@ -386,10 +372,12 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): operation="request certificate" takes_options = ( - Str( + Principal( 'principal', + validate_realm, label=_('Principal'), doc=_('Principal for this certificate (e.g. HTTP/test.example.com)'), + normalizer=normalize_principal ), Flag( 'add', @@ -432,27 +420,29 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): taskgroup (directly or indirectly via role membership). """ - principal_string = kw.get('principal') - principal = split_any_principal(principal_string) - servicename, principal_name, realm = principal - if servicename is None: + principal = kw.get('principal') + principal_string = unicode(principal) + + if principal.is_user: principal_type = USER - elif servicename == 'host': + elif principal.is_host: principal_type = HOST else: principal_type = SERVICE - bind_principal = split_any_principal(getattr(context, 'principal')) - bind_service, bind_name, bind_realm = bind_principal + bind_principal = kerberos.Principal( + getattr(context, 'principal')) + bind_principal_string = unicode(bind_principal) - if bind_service is None: + if bind_principal.is_user: bind_principal_type = USER - elif bind_service == 'host': + elif bind_principal.is_host: bind_principal_type = HOST else: bind_principal_type = SERVICE - if bind_principal != principal and bind_principal_type != HOST: + if (bind_principal_string != principal_string and + bind_principal_type != HOST): # Can the bound principal request certs for another principal? self.check_access() @@ -463,7 +453,7 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): bypass_caacl = False if not bypass_caacl: - caacl_check(principal_type, principal_string, ca, profile_id) + caacl_check(principal_type, principal, ca, profile_id) try: subject = pkcs10.get_subject(csr) @@ -474,7 +464,8 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): error=_("Failure decoding Certificate Signing Request: %s") % e) # self-service and host principals may bypass SAN permission check - if bind_principal != principal and bind_principal_type != HOST: + if (bind_principal_string != principal_string + and bind_principal_type != HOST): if '2.5.29.17' in extensions: self.check_access('request certificate with subjectaltname') @@ -486,9 +477,11 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): if principal_type == SERVICE: principal_obj = api.Command['service_show'](principal_string, all=True) elif principal_type == HOST: - principal_obj = api.Command['host_show'](principal_name, all=True) + principal_obj = api.Command['host_show']( + principal.hostname, all=True) elif principal_type == USER: - principal_obj = api.Command['user_show'](principal_name, all=True) + principal_obj = api.Command['user_show']( + principal.username, all=True) except errors.NotFound as e: if add: if principal_type == SERVICE: @@ -512,14 +505,14 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): error=_("No Common Name was found in subject of request.")) if principal_type in (SERVICE, HOST): - if cn.lower() != principal_name.lower(): + if cn.lower() != principal.hostname.lower(): raise errors.ACIError( info=_("hostname in subject of request '%(cn)s' " "does not match principal hostname '%(hostname)s'") - % dict(cn=cn, hostname=principal_name)) + % dict(cn=cn, hostname=principal.hostname)) elif principal_type == USER: # check user name - if cn != principal_name: + if cn != principal.username: raise errors.ValidationError( name='csr', error=_("DN commonName does not match user's login") @@ -545,13 +538,11 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): if name_type == pkcs10.SAN_DNSNAME: name = unicode(name) alt_principal_obj = None - alt_principal_string = None + alt_principal_string = unicode(principal) try: if principal_type == HOST: - alt_principal_string = 'host/%s@%s' % (name, realm) alt_principal_obj = api.Command['host_show'](name, all=True) elif principal_type == SERVICE: - alt_principal_string = '%s/%s@%s' % (servicename, name, realm) alt_principal_obj = api.Command['service_show']( alt_principal_string, all=True) elif principal_type == USER: @@ -574,11 +565,10 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): "Insufficient privilege to create a certificate " "with subject alt name '%s'.") % name) if alt_principal_string is not None and not bypass_caacl: - caacl_check( - principal_type, alt_principal_string, ca, profile_id) + caacl_check(principal_type, principal, ca, profile_id) elif name_type in (pkcs10.SAN_OTHERNAME_KRB5PRINCIPALNAME, pkcs10.SAN_OTHERNAME_UPN): - if split_any_principal(name) != principal: + if name != principal_string: raise errors.ACIError( info=_("Principal '%s' in subject alt name does not " "match requested principal") % name) @@ -619,9 +609,9 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): if principal_type == SERVICE: api.Command['service_mod'](principal_string, **kwargs) elif principal_type == HOST: - api.Command['host_mod'](principal_name, **kwargs) + api.Command['host_mod'](principal.hostname, **kwargs) elif principal_type == USER: - api.Command['user_mod'](principal_name, **kwargs) + api.Command['user_mod'](principal.username, **kwargs) return dict( result=result, @@ -748,10 +738,10 @@ class cert_show(Retrieve, CertMethod, VirtualCommand): self.check_access() except errors.ACIError as acierr: self.debug("Not granted by ACI to retrieve certificate, looking at principal") - bind_principal = getattr(context, 'principal') - if not bind_principal.startswith('host/'): + bind_principal = kerberos.Principal(getattr(context, 'principal')) + if not bind_principal.is_host: raise acierr - hostname = get_host_from_principal(bind_principal) + hostname = bind_principal.hostname ca_obj = api.Command.ca_show(options['cacn'])['result'] issuer_dn = ca_obj['ipacasubjectdn'][0] diff --git a/ipaserver/plugins/host.py b/ipaserver/plugins/host.py index de0aca5ca..6210e8c16 100644 --- a/ipaserver/plugins/host.py +++ b/ipaserver/plugins/host.py @@ -25,6 +25,7 @@ import six from ipalib import api, errors, util from ipalib import messages from ipalib import Str, Flag, Bytes +from ipalib.parameters import Principal from ipalib.plugable import Registry from .baseldap import (LDAPQuery, LDAPObject, LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, @@ -32,7 +33,8 @@ from .baseldap import (LDAPQuery, LDAPObject, LDAPCreate, LDAPRemoveMember, host_is_master, pkey_to_value, add_missing_object_class, LDAPAddAttribute, LDAPRemoveAttribute) -from .service import (split_principal, validate_certificate, +from ipaserver.plugins.service import ( + validate_realm, normalize_principal, validate_certificate, set_certificate_attrs, ticket_flags_params, update_krbticketflags, set_kerberos_attrs, rename_ipaallowedtoperform_from_ldap, rename_ipaallowedtoperform_to_ldap, revoke_certs) @@ -56,6 +58,7 @@ from ipapython.ipautil import ipa_generate_password, CheckedIPAddress from ipapython.dnsutil import DNSName from ipapython.ssh import SSHPublicKey from ipapython.dn import DN +from ipapython import kerberos from functools import reduce if six.PY3: @@ -509,8 +512,11 @@ class host(LDAPObject): label=_('Revocation reason'), flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, ), - Str('krbprincipalname?', + Principal( + 'krbprincipalname?', + validate_realm, label=_('Principal name'), + normalizer=normalize_principal, flags=['no_create', 'no_update', 'no_search'], ), Str('macaddress*', @@ -758,8 +764,9 @@ class host_del(LDAPDelete): break else: for entry_attrs in services: - principal = entry_attrs['krbprincipalname'][0] - (service, hostname, realm) = split_principal(principal) + principal = kerberos.Principal( + entry_attrs['krbprincipalname'][0]) + hostname = principal.hostname if hostname.lower() == fqdn: api.Command['service_del'](principal) updatedns = options.get('updatedns', False) @@ -830,10 +837,13 @@ class host_mod(LDAPUpdate): member_attributes = ['managedby'] takes_options = LDAPUpdate.takes_options + ( - Str('krbprincipalname?', + Principal( + 'krbprincipalname?', + validate_realm, cli_name='principalname', label=_('Principal name'), doc=_('Kerberos principal name for this host'), + normalizer=normalize_principal, attribute=True, ), Flag('updatedns?', @@ -1155,8 +1165,9 @@ class host_disable(LDAPQuery): break else: for entry_attrs in services: - principal = entry_attrs['krbprincipalname'][0] - (service, hostname, realm) = split_principal(principal) + principal = kerberos.Principal( + entry_attrs['krbprincipalname'][0]) + hostname = principal.hostname if hostname.lower() == fqdn: try: api.Command['service_disable'](principal) diff --git a/ipaserver/plugins/passwd.py b/ipaserver/plugins/passwd.py index 253a0d35d..1576c4ca8 100644 --- a/ipaserver/plugins/passwd.py +++ b/ipaserver/plugins/passwd.py @@ -17,15 +17,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import six + from ipalib import api, errors, krb_utils from ipalib import Command -from ipalib import Str, Password +from ipalib import Password from ipalib import _ from ipalib import output +from ipalib.parameters import Principal from ipalib.plugable import Registry -from .baseuser import validate_principal, normalize_principal from ipalib.request import context +from ipapython import kerberos from ipapython.dn import DN +from ipaserver.plugins.service import validate_realm, normalize_principal + +if six.PY3: + unicode = str __doc__ = _(""" Set a user's password @@ -59,7 +66,7 @@ def get_current_password(principal): be ignored later. """ current_principal = krb_utils.get_principal() - if current_principal == normalize_principal(principal): + if current_principal == unicode(normalize_principal(principal)): return None else: return MAGIC_VALUE @@ -69,12 +76,14 @@ class passwd(Command): __doc__ = _("Set a user's password.") takes_args = ( - Str('principal', validate_principal, + Principal( + 'principal', + validate_realm, cli_name='user', label=_('User name'), primary_key=True, autofill=True, - default_from=lambda: krb_utils.get_principal(), + default_from=lambda: kerberos.Principal(krb_utils.get_principal()), normalizer=lambda value: normalize_principal(value), ), Password('password', @@ -114,6 +123,8 @@ class passwd(Command): """ ldap = self.api.Backend.ldap2 + principal = unicode(principal) + entry_attrs = ldap.find_entry_by_attr( 'krbprincipalname', principal, 'posixaccount', [''], DN(api.env.container_user, api.env.basedn) diff --git a/ipaserver/plugins/service.py b/ipaserver/plugins/service.py index c44ad7ac2..70bf31fd4 100644 --- a/ipaserver/plugins/service.py +++ b/ipaserver/plugins/service.py @@ -23,6 +23,7 @@ import six from ipalib import api, errors, messages from ipalib import Bytes, StrEnum, Bool, Str, Flag +from ipalib.parameters import Principal from ipalib.plugable import Registry from .baseldap import ( host_is_master, @@ -43,6 +44,7 @@ from ipalib import x509 from ipalib import _, ngettext from ipalib import util from ipalib import output +from ipapython import kerberos from ipapython.dn import DN import nss.nss as nss @@ -176,55 +178,29 @@ _ticket_flags_map = { _ticket_flags_default = _ticket_flags_map['ipakrbrequirespreauth'] -def split_any_principal(principal): - service = hostname = realm = None - - # Break down the principal into its component parts, which may or - # may not include the realm. - sp = principal.split('/') - name_and_realm = None - if len(sp) > 2: - raise errors.MalformedServicePrincipal(reason=_('unable to determine service')) - elif len(sp) == 2: - service = sp[0] - if len(service) == 0: - raise errors.MalformedServicePrincipal(reason=_('blank service')) - name_and_realm = sp[1] - else: - name_and_realm = sp[0] - - sr = name_and_realm.split('@') - if len(sr) > 2: - raise errors.MalformedServicePrincipal( - reason=_('unable to determine realm')) - - hostname = sr[0].lower() - if len(sr) == 2: - realm = sr[1].upper() - # At some point we'll support multiple realms - if realm != api.env.realm: - raise errors.RealmMismatch() - else: - realm = api.env.realm - - # Note that realm may be None. - return service, hostname, realm - -def split_principal(principal): - service, name, realm = split_any_principal(principal) - if service is None: - raise errors.MalformedServicePrincipal(reason=_('missing service')) - return service, name, realm - -def validate_principal(ugettext, principal): - (service, hostname, principal) = split_principal(principal) - return None - -def normalize_principal(principal): - # The principal is already validated when it gets here - (service, hostname, realm) = split_principal(principal) - # Put the principal back together again - principal = '%s/%s@%s' % (service, hostname, realm) + +def validate_realm(ugettext, principal): + """ + Check that the principal's realm matches IPA realm if present + """ + realm = principal.realm + if realm is not None and realm != api.env.realm: + raise errors.RealmMismatch() + + +def normalize_principal(value): + """ + Ensure that the name in the principal is lower-case. The realm is + upper-case by convention but it isn't required. + + The principal is validated at this point. + """ + try: + principal = kerberos.Principal(value, realm=api.env.realm) + except ValueError: + raise errors.ValidationError( + name='principal', reason=_("Malformed principal")) + return unicode(principal) def validate_certificate(ugettext, cert): @@ -291,16 +267,16 @@ def set_certificate_attrs(entry_attrs): entry_attrs['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0]) entry_attrs['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0]) -def check_required_principal(ldap, hostname, service): +def check_required_principal(ldap, principal): """ Raise an error if the host of this prinicipal is an IPA master and one of the principals required for proper execution. """ try: - host_is_master(ldap, hostname) + host_is_master(ldap, principal.hostname) except errors.ValidationError as e: service_types = ['HTTP', 'ldap', 'DNS', 'dogtagldap'] - if service in service_types: + if principal.service_name in service_types: raise errors.ValidationError(name='principal', error=_('This principal is required by the IPA master')) def update_krbticketflags(ldap, entry_attrs, attrs_list, options, existing): @@ -457,12 +433,15 @@ class service(LDAPObject): label_singular = _('Service') takes_params = ( - Str('krbprincipalname', validate_principal, + Principal( + 'krbprincipalname', + validate_realm, cli_name='principal', label=_('Principal'), doc=_('Service principal'), primary_key=True, - normalizer=lambda value: normalize_principal(value), + normalizer=normalize_principal, + require_service=True ), Bytes('usercertificate*', validate_certificate, cli_name='certificate', @@ -560,8 +539,10 @@ class service_add(LDAPCreate): def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): assert isinstance(dn, DN) - (service, hostname, realm) = split_principal(keys[-1]) - if service.lower() == 'host' and not options['force']: + principal = keys[-1] + hostname = principal.hostname + + if principal.is_host and not options['force']: raise errors.HostService() try: @@ -619,8 +600,7 @@ class service_del(LDAPDelete): # In the case of services we don't want IPA master services to be # deleted. This is a limited few though. If the user has their own # custom services allow them to manage them. - (service, hostname, realm) = split_principal(keys[-1]) - check_required_principal(ldap, hostname, service) + check_required_principal(ldap, keys[-1]) if self.api.Command.ca_is_enabled()['result']: try: entry_attrs = ldap.get_entry(dn, ['usercertificate']) @@ -647,8 +627,6 @@ class service_mod(LDAPUpdate): self.obj.validate_ipakrbauthzdata(entry_attrs) - (service, hostname, realm) = split_principal(keys[-1]) - # verify certificates certs = entry_attrs.get('usercertificate') or [] certs_der = [x509.normalize_certificate(c) for c in certs] @@ -873,8 +851,7 @@ class service_disable(LDAPQuery): dn = self.obj.get_dn(*keys, **options) entry_attrs = ldap.get_entry(dn, ['usercertificate']) - (service, hostname, realm) = split_principal(keys[-1]) - check_required_principal(ldap, hostname, service) + check_required_principal(ldap, keys[-1]) # See if we do any work at all here and if not raise an exception done_work = False diff --git a/ipaserver/plugins/vault.py b/ipaserver/plugins/vault.py index 380e4d478..c9b7cb942 100644 --- a/ipaserver/plugins/vault.py +++ b/ipaserver/plugins/vault.py @@ -17,25 +17,31 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import six + from ipalib.frontend import Command, Object from ipalib import api, errors from ipalib import Bytes, Flag, Str, StrEnum from ipalib import output from ipalib.crud import PKQuery, Retrieve +from ipalib.parameters import Principal from ipalib.plugable import Registry from .baseldap import LDAPObject, LDAPCreate, LDAPDelete,\ LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,\ LDAPModMember, pkey_to_value from ipalib.request import context -from .baseuser import split_principal -from .service import normalize_principal +from .service import normalize_principal, validate_realm from ipalib import _, ngettext +from ipapython import kerberos from ipapython.dn import DN if api.env.in_server: import pki.account import pki.key +if six.PY3: + unicode = str + __doc__ = _(""" Vaults """) + _(""" @@ -191,8 +197,9 @@ EXAMPLES: register = Registry() vault_options = ( - Str( + Principal( 'service?', + validate_realm, doc=_('Service name of the service vault'), normalizer=normalize_principal, ), @@ -342,17 +349,15 @@ class vaultcontainer(LDAPObject): parent_dn = super(vaultcontainer, self).get_dn(*keys, **options) if not count: - principal = getattr(context, 'principal') + principal = kerberos.Principal(getattr(context, 'principal')) - if principal.startswith('host/'): + if principal.is_host: raise errors.NotImplementedError( reason=_('Host is not supported')) - - (name, realm) = split_principal(principal) - if '/' in name: - service = principal + elif principal.is_service: + service = unicode(principal) else: - user = name + user = principal.username if service: dn = DN(('cn', service), ('cn', 'services'), parent_dn) @@ -660,17 +665,15 @@ class vault(LDAPObject): rdns = DN(*dn[:-len(container_dn)]) if not count: - principal = getattr(context, 'principal') + principal = kerberos.Principal(getattr(context, 'principal')) - if principal.startswith('host/'): + if principal.is_host: raise errors.NotImplementedError( reason=_('Host is not supported')) - - (name, realm) = split_principal(principal) - if '/' in name: - service = principal + elif principal.is_service: + service = unicode(principal) else: - user = name + user = principal.username if service: parent_dn = DN(('cn', service), ('cn', 'services'), container_dn) @@ -770,12 +773,11 @@ class vault_add_internal(LDAPCreate): raise errors.InvocationError( format=_('KRA service is not enabled')) - principal = getattr(context, 'principal') - (name, realm) = split_principal(principal) - if '/' in name: - owner_dn = self.api.Object.service.get_dn(name) + principal = kerberos.Principal(getattr(context, 'principal')) + if principal.is_service: + owner_dn = self.api.Object.service.get_dn(unicode(principal)) else: - owner_dn = self.api.Object.user.get_dn(name) + owner_dn = self.api.Object.user.get_dn(principal.username) parent_dn = DN(*dn[1:]) diff --git a/ipatests/test_xmlrpc/test_host_plugin.py b/ipatests/test_xmlrpc/test_host_plugin.py index e6fc68a15..4ddabefff 100644 --- a/ipatests/test_xmlrpc/test_host_plugin.py +++ b/ipatests/test_xmlrpc/test_host_plugin.py @@ -357,6 +357,7 @@ class TestHostWithService(XMLRPC_test): result=dict( dn=service1dn, krbprincipalname=[service1], + krbcanonicalname=[service1], objectclass=objectclasses.service, managedby_host=[host.fqdn], ipauniqueid=[fuzzy_uuid], diff --git a/ipatests/test_xmlrpc/test_service_plugin.py b/ipatests/test_xmlrpc/test_service_plugin.py index 2e38b8d6b..69af06873 100644 --- a/ipatests/test_xmlrpc/test_service_plugin.py +++ b/ipatests/test_xmlrpc/test_service_plugin.py @@ -193,6 +193,7 @@ class test_service(Declarative): result=dict( dn=service1dn, krbprincipalname=[service1], + krbcanonicalname=[service1], objectclass=objectclasses.service, ipauniqueid=[fuzzy_uuid], managedby_host=[fqdn1], @@ -262,6 +263,7 @@ class test_service(Declarative): dict( dn=service1dn, krbprincipalname=[service1], + krbcanonicalname=service1, managedby_host=[fqdn1], has_keytab=False, ), @@ -281,6 +283,7 @@ class test_service(Declarative): dict( dn=service1dn, krbprincipalname=[service1], + krbcanonicalname=service1, has_keytab=False, ), ], @@ -619,7 +622,9 @@ class test_service(Declarative): dict( desc='Create service with malformed principal "foo"', command=('service_add', [u'foo'], {}), - expected=errors.MalformedServicePrincipal(reason='missing service') + expected=errors.ValidationError( + name='principal', + error='Service principal is required') ), @@ -715,6 +720,7 @@ class test_service_in_role(Declarative): result=dict( dn=service1dn, krbprincipalname=[service1], + krbcanonicalname=[service1], objectclass=objectclasses.service, ipauniqueid=[fuzzy_uuid], managedby_host=[fqdn1], @@ -919,6 +925,7 @@ class test_service_allowed_to(Declarative): result=dict( dn=service1dn, krbprincipalname=[service1], + krbcanonicalname=[service1], objectclass=objectclasses.service, ipauniqueid=[fuzzy_uuid], managedby_host=[fqdn1], diff --git a/ipatests/test_xmlrpc/test_stageuser_plugin.py b/ipatests/test_xmlrpc/test_stageuser_plugin.py index 96f7e22b3..34cfaf87a 100644 --- a/ipatests/test_xmlrpc/test_stageuser_plugin.py +++ b/ipatests/test_xmlrpc/test_stageuser_plugin.py @@ -345,9 +345,9 @@ class TestCreateInvalidAttributes(XMLRPC_test): stageduser.ensure_missing() command = stageduser.make_create_command( options={u'krbprincipalname': invalidrealm2}) - with raises_exact(errors.MalformedUserPrincipal( - message=u'Principal is not of the form user@REALM: \'%s\'' % - invalidrealm2)): + with raises_exact(errors.ConversionError( + name='principal', error="Malformed principal: '{}'".format( + invalidrealm2))): command() diff --git a/ipatests/test_xmlrpc/test_user_plugin.py b/ipatests/test_xmlrpc/test_user_plugin.py index 6d58c53aa..8245dc7f0 100644 --- a/ipatests/test_xmlrpc/test_user_plugin.py +++ b/ipatests/test_xmlrpc/test_user_plugin.py @@ -806,8 +806,9 @@ class TestPrincipals(XMLRPC_test): ) command = testuser.make_create_command() - with raises_exact(errors.MalformedUserPrincipal( - principal=u'tuser1@BAD@NOTFOUND.ORG')): + with raises_exact(errors.ConversionError( + name='principal', error="Malformed principal: '{}'".format( + testuser.kwargs['krbprincipalname']))): command() def test_set_principal_expiration(self, user): -- cgit