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 pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): assert isinstance(dn, DN) if not self.api.Command.kra_is_enabled()['result']: raise errors.InvocationError( format=_('KRA service is not enabled')) 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(principal.username) parent_dn = DN(*dn[1:]) try: self.obj.create_container(parent_dn, owner_dn) except (errors.DuplicateEntry, errors.ACIError): pass # vault should be owned by the creator entry_attrs['owner'] = owner_dn return dn
def principal(self): if any(attr is None for attr in (self.realm, self.fqdn, self.service_prefix)): return return unicode( kerberos.Principal((self.service_prefix, self.fqdn), realm=self.realm))
def get_dn(self, *keys, **kwargs): key = keys[0] if isinstance(key, six.text_type): key = kerberos.Principal(key) key = unicode(normalize_principal(key)) parent_dn = DN(self.container_dn, self.api.env.basedn) true_rdn = 'krbprincipalname' return self.backend.make_dn_from_attr(true_rdn, key, parent_dn)
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 bind_principal_can_manage_cert(cert): """Check that the bind principal can manage the given cert. ``cert`` An NSS certificate object. """ bind_principal = kerberos.Principal(getattr(context, 'principal')) if not bind_principal.is_host: return False hostname = bind_principal.hostname # If we have a hostname we want to verify that the subject # of the certificate matches it. return hostname == cert.subject.common_name #pylint: disable=E1101
def get_dn(self, *keys, **options): """ Generates vault DN from parameters. """ service = options.get('service') shared = options.get('shared') user = options.get('username') count = (bool(service) + bool(shared) + bool(user)) if count > 1: raise errors.MutuallyExclusiveError( reason=_('Service, shared, and user options ' + 'cannot be specified simultaneously')) # TODO: create container_dn after object initialization then reuse it container_dn = DN(self.container_dn, self.api.env.basedn) dn = super(vault, self).get_dn(*keys, **options) assert dn.endswith(container_dn) rdns = DN(*dn[:-len(container_dn)]) if not count: principal = kerberos.Principal(getattr(context, 'principal')) if principal.is_host: raise errors.NotImplementedError( reason=_('Host is not supported')) elif principal.is_service: service = unicode(principal) else: user = principal.username if service: parent_dn = DN(('cn', service), ('cn', 'services'), container_dn) elif shared: parent_dn = DN(('cn', 'shared'), container_dn) elif user: parent_dn = DN(('cn', user), ('cn', 'users'), container_dn) else: raise RuntimeError return DN(rdns, parent_dn)
def bind_principal_can_manage_cert(cert): """Check that the bind principal can manage the given cert. ``cert`` A python-cryptography ``Certificate`` object. """ bind_principal = kerberos.Principal(getattr(context, 'principal')) if not bind_principal.is_host: return False hostname = bind_principal.hostname # Verify that hostname matches subject of cert. # We check the "most-specific" CN value. cns = cert.subject.get_attributes_for_oid( cryptography.x509.oid.NameOID.COMMON_NAME) if len(cns) == 0: return False # no CN in subject else: return hostname == cns[-1].value
def get_dn(self, *keys, **options): """ Generates vault DN from parameters. """ service = options.get('service') shared = options.get('shared') user = options.get('username') count = (bool(service) + bool(shared) + bool(user)) if count > 1: raise errors.MutuallyExclusiveError( reason=_('Service, shared and user options ' + 'cannot be specified simultaneously')) parent_dn = super(vaultcontainer, self).get_dn(*keys, **options) if not count: principal = kerberos.Principal(getattr(context, 'principal')) if principal.is_host: raise errors.NotImplementedError( reason=_('Host is not supported')) elif principal.is_service: service = unicode(principal) else: user = principal.username if service: dn = DN(('cn', service), ('cn', 'services'), parent_dn) elif shared: dn = DN(('cn', 'shared'), parent_dn) elif user: dn = DN(('cn', user), ('cn', 'users'), parent_dn) else: raise RuntimeError return dn
def validate_auth_indicator(entry): new_value = entry.get('krbprincipalauthind', None) if not new_value: return # The following services are considered internal IPA services # and shouldn't be allowed to have auth indicators. # https://pagure.io/freeipa/issue/8206 pkey = api.Object['service'].get_primary_key_from_dn(entry.dn) if pkey == str(entry.dn): # krbcanonicalname may not be set yet if this is a host entry, # try krbprincipalname if 'krbprincipalname' in entry: pkey = entry['krbprincipalname'] principal = kerberos.Principal(pkey) server = api.Command.server_find(principal.hostname)['result'] if server: prefixes = ("host", "cifs", "ldap", "HTTP") else: prefixes = ("cifs", ) if principal.service_name in prefixes: raise errors.ValidationError( name='krbprincipalauthind', error=_('authentication indicators not allowed ' 'in service "%s"' % principal.service_name))
class baseuser(LDAPObject): """ baseuser object. """ stage_container_dn = api.env.container_stageuser active_container_dn = api.env.container_user delete_container_dn = api.env.container_deleteuser object_class = ['posixaccount'] object_class_config = 'ipauserobjectclasses' possible_objectclasses = [ 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', 'ipatokenradiusproxyuser', 'ipacertmapobject' ] disallow_object_classes = ['krbticketpolicyaux'] permission_filter_objectclasses = ['posixaccount'] search_attributes_config = 'ipausersearchfields' default_attributes = [ 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', 'uidnumber', 'gidnumber', 'mail', 'ou', 'telephonenumber', 'title', 'memberof', 'nsaccountlock', 'memberofindirect', 'ipauserauthtype', 'userclass', 'ipatokenradiusconfiglink', 'ipatokenradiususername', 'krbprincipalexpiration', 'usercertificate;binary', 'krbprincipalname', 'krbcanonicalname', 'ipacertmapdata' ] search_display_attributes = [ 'uid', 'givenname', 'sn', 'homedirectory', 'krbcanonicalname', 'krbprincipalname', 'loginshell', 'mail', 'telephonenumber', 'title', 'nsaccountlock', 'uidnumber', 'gidnumber', 'sshpubkeyfp', ] uuid_attribute = 'ipauniqueid' attribute_members = { 'manager': ['user'], 'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], 'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], } allow_rename = True bindable = True password_attributes = [('userpassword', 'has_password'), ('krbprincipalkey', 'has_keytab')] label = _('Users') label_singular = _('User') takes_params = ( Str('uid', pattern=PATTERN_GROUPUSER_NAME, pattern_errmsg='may only include letters, numbers, _, -, . and $', maxlength=255, cli_name='login', label=_('User login'), primary_key=True, default_from=lambda givenname, sn: givenname[0] + sn, normalizer=lambda value: value.lower(), ), Str('givenname', cli_name='first', label=_('First name'), ), Str('sn', cli_name='last', label=_('Last name'), ), Str('cn', label=_('Full name'), default_from=lambda givenname, sn: '%s %s' % (givenname, sn), autofill=True, ), Str('displayname?', label=_('Display name'), default_from=lambda givenname, sn: '%s %s' % (givenname, sn), autofill=True, ), Str('initials?', label=_('Initials'), default_from=lambda givenname, sn: '%c%c' % (givenname[0], sn[0]), autofill=True, ), Str('homedirectory?', cli_name='homedir', label=_('Home directory'), ), Str('gecos?', label=_('GECOS'), default_from=lambda givenname, sn: '%s %s' % (givenname, sn), autofill=True, ), Str('loginshell?', cli_name='shell', label=_('Login shell'), ), Principal( 'krbcanonicalname?', validate_realm, label=_('Principal name'), flags={'no_option', 'no_create', 'no_update', 'no_search'}, normalizer=normalize_user_principal ), Principal( 'krbprincipalname*', validate_realm, cli_name='principal', label=_('Principal alias'), default_from=lambda uid: kerberos.Principal( uid.lower(), realm=api.env.realm), autofill=True, normalizer=normalize_user_principal, ), DateTime('krbprincipalexpiration?', cli_name='principal_expiration', label=_('Kerberos principal expiration'), ), DateTime('krbpasswordexpiration?', cli_name='password_expiration', label=_('User password expiration'), ), Str('mail*', cli_name='email', label=_('Email address'), ), Password('userpassword?', cli_name='password', label=_('Password'), doc=_('Prompt to set the user password'), # FIXME: This is temporary till bug is fixed causing updates to # bomb out via the webUI. exclude='webui', ), Flag('random?', doc=_('Generate a random user password'), flags=('no_search', 'virtual_attribute'), default=False, ), Str('randompassword?', label=_('Random password'), flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'), ), Int('uidnumber?', cli_name='uid', label=_('UID'), doc=_('User ID Number (system will assign one if not provided)'), minvalue=1, ), Int('gidnumber?', label=_('GID'), doc=_('Group ID Number'), minvalue=1, ), Str('street?', cli_name='street', label=_('Street address'), ), Str('l?', cli_name='city', label=_('City'), ), Str('st?', cli_name='state', label=_('State/Province'), ), Str('postalcode?', label=_('ZIP'), ), Str('telephonenumber*', cli_name='phone', label=_('Telephone Number') ), Str('mobile*', label=_('Mobile Telephone Number') ), Str('pager*', label=_('Pager Number') ), Str('facsimiletelephonenumber*', cli_name='fax', label=_('Fax Number'), ), Str('ou?', cli_name='orgunit', label=_('Org. Unit'), ), Str('title?', label=_('Job Title'), ), # keep backward compatibility using single value manager option Str('manager?', label=_('Manager'), ), Str('carlicense*', label=_('Car License'), ), Str('ipasshpubkey*', validate_sshpubkey, cli_name='sshpubkey', label=_('SSH public key'), normalizer=normalize_sshpubkey, flags=['no_search'], ), Str('sshpubkeyfp*', label=_('SSH public key fingerprint'), flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, ), StrEnum('ipauserauthtype*', cli_name='user_auth_type', label=_('User authentication types'), doc=_('Types of supported user authentication'), values=(u'password', u'radius', u'otp'), ), Str('userclass*', cli_name='class', label=_('Class'), doc=_('User category (semantics placed on this attribute are for ' 'local interpretation)'), ), Str('ipatokenradiusconfiglink?', cli_name='radius', label=_('RADIUS proxy configuration'), ), Str('ipatokenradiususername?', cli_name='radius_username', label=_('RADIUS proxy username'), ), Str('departmentnumber*', label=_('Department Number'), ), Str('employeenumber?', label=_('Employee Number'), ), Str('employeetype?', label=_('Employee Type'), ), Str('preferredlanguage?', label=_('Preferred Language'), pattern='^(([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?' \ + '(\s*,\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?)*)|(\*))$', pattern_errmsg='must match RFC 2068 - 14.4, e.g., "da, en-gb;q=0.8, en;q=0.7"', ), Bytes('usercertificate*', validate_certificate, cli_name='certificate', label=_('Certificate'), doc=_('Base-64 encoded user certificate'), ), Str( 'ipacertmapdata*', cli_name='certmapdata', label=_('Certificate mapping data'), doc=_('Certificate mapping data'), flags=['no_create', 'no_update', 'no_search'], ), ) def normalize_and_validate_email(self, email, config=None): if not config: config = self.backend.get_ipa_config() # check if default email domain should be added defaultdomain = config.get('ipadefaultemaildomain', [None])[0] if email: norm_email = [] if not isinstance(email, (list, tuple)): email = [email] for m in email: if isinstance(m, six.string_types): if '@' not in m and defaultdomain: m = m + u'@' + defaultdomain if not Email(m): raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) norm_email.append(m) else: if not Email(m): raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) norm_email.append(m) return norm_email return email def normalize_manager(self, manager, container): """ Given a userid verify the user's existence (in the appropriate containter) and return the dn. """ if not manager: return None if not isinstance(manager, list): manager = [manager] try: container_dn = DN(container, api.env.basedn) for i, mgr in enumerate(manager): if isinstance(mgr, DN) and mgr.endswith(container_dn): continue entry_attrs = self.backend.find_entry_by_attr( self.primary_key.name, mgr, self.object_class, [''], container_dn ) manager[i] = entry_attrs.dn except errors.NotFound: raise errors.NotFound(reason=_('manager %(manager)s not found') % dict(manager=mgr)) return manager def _user_status(self, user, container): assert isinstance(user, DN) return user.endswith(container) def active_user(self, user): assert isinstance(user, DN) return self._user_status(user, DN(self.active_container_dn, api.env.basedn)) def stage_user(self, user): assert isinstance(user, DN) return self._user_status(user, DN(self.stage_container_dn, api.env.basedn)) def delete_user(self, user): assert isinstance(user, DN) return self._user_status(user, DN(self.delete_container_dn, api.env.basedn)) def convert_usercertificate_pre(self, entry_attrs): if 'usercertificate' in entry_attrs: entry_attrs['usercertificate;binary'] = entry_attrs.pop( 'usercertificate') def convert_usercertificate_post(self, entry_attrs, **options): if 'usercertificate;binary' in entry_attrs: entry_attrs['usercertificate'] = entry_attrs.pop( 'usercertificate;binary') def convert_attribute_members(self, entry_attrs, *keys, **options): super(baseuser, self).convert_attribute_members( entry_attrs, *keys, **options) if options.get("raw", False): return # due the backward compatibility, managers have to be returned in # 'manager' attribute instead of 'manager_user' try: entry_attrs['failed_manager'] = entry_attrs.pop('manager') except KeyError: pass try: entry_attrs['manager'] = entry_attrs.pop('manager_user') except KeyError: pass
class passwd(Command): __doc__ = _("Set a user's password.") takes_args = ( Principal( 'principal', validate_realm, cli_name='user', label=_('User name'), primary_key=True, autofill=True, default_from=lambda: kerberos.Principal(krb_utils.get_principal()), normalizer=lambda value: normalize_user_principal(value), ), Password( 'password', label=_('New Password'), ), Password( 'current_password', label=_('Current Password'), confirm=False, default_from=lambda principal: get_current_password(principal), autofill=True, ), ) takes_options = (Password( 'otp?', label=_('OTP'), doc=_('The OTP if the user has a token configured'), confirm=False, ), ) has_output = output.simple_value msg_summary = _('Changed password for "%(value)s"') def execute(self, principal, password, current_password, **options): """ Execute the passwd operation. The dn should not be passed as a keyword argument as it is constructed by this method. Returns the entry :param principal: The login name or principal of the user :param password: the new password :param current_password: the existing password, if applicable """ 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)) if principal == getattr(context, 'principal') and \ current_password == MAGIC_VALUE: # No cheating logger.warning('User attempted to change password using magic ' 'value') raise errors.ACIError(info=_('Invalid credentials')) if current_password == MAGIC_VALUE: ldap.modify_password(entry_attrs.dn, password) else: otp = options.get('otp') ldap.modify_password(entry_attrs.dn, password, current_password, otp) return dict( result=True, value=principal, )
def execute(self, csr, all=False, raw=False, **kw): ca_enabled_check() ldap = self.api.Backend.ldap2 add = kw.get('add') request_type = kw.get('request_type') profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE) # Check that requested authority exists (done before CA ACL # enforcement so that user gets better error message if # referencing nonexistant CA) and look up authority ID. # ca = kw['cacn'] ca_obj = api.Command.ca_show(ca)['result'] ca_id = ca_obj['ipacaid'][0] """ Access control is partially handled by the ACI titled 'Hosts can modify service userCertificate'. This is for the case where a machine binds using a host/ prinicpal. It can only do the request if the target hostname is in the managedBy attribute which is managed using the add/del member commands. Binding with a user principal one needs to be in the request_certs taskgroup (directly or indirectly via role membership). """ principal = kw.get('principal') principal_string = unicode(principal) if principal.is_user: principal_type = USER elif principal.is_host: principal_type = HOST else: principal_type = SERVICE bind_principal = kerberos.Principal(getattr(context, 'principal')) bind_principal_string = unicode(bind_principal) if bind_principal.is_user: bind_principal_type = USER elif bind_principal.is_host: bind_principal_type = HOST else: bind_principal_type = SERVICE if (bind_principal_string != principal_string and bind_principal_type != HOST): # Can the bound principal request certs for another principal? self.check_access() try: self.check_access("request certificate ignore caacl") bypass_caacl = True except errors.ACIError: bypass_caacl = False if not bypass_caacl: caacl_check(principal_type, principal, ca, profile_id) try: subject = pkcs10.get_subject(csr) extensions = pkcs10.get_extensions(csr) subjectaltname = pkcs10.get_subjectaltname(csr) or () except (NSPRError, PyAsn1Error, ValueError) as e: raise errors.CertificateOperationError( error=_("Failure decoding Certificate Signing Request: %s") % e) # self-service and host principals may bypass SAN permission check 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') dn = None principal_obj = None # See if the service exists and punt if it doesn't and we aren't # going to add it try: 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.hostname, all=True) elif principal_type == USER: principal_obj = api.Command['user_show'](principal.username, all=True) except errors.NotFound as e: if add: if principal_type == SERVICE: principal_obj = api.Command['service_add']( principal_string, force=True) else: princtype_str = PRINCIPAL_TYPE_STRING_MAP[principal_type] raise errors.OperationNotSupportedForPrincipalType( operation=_("'add' option"), principal_type=princtype_str) else: raise errors.NotFound( reason=_("The principal for this request doesn't exist.")) principal_obj = principal_obj['result'] dn = principal_obj['dn'] # Ensure that the DN in the CSR matches the principal cn = subject.common_name #pylint: disable=E1101 if not cn: raise errors.ValidationError( name='csr', error=_("No Common Name was found in subject of request.")) if principal_type in (SERVICE, HOST): 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.hostname)) elif principal_type == USER: # check user name if cn != principal.username: raise errors.ValidationError( name='csr', error=_("DN commonName does not match user's login")) # check email address mail = subject.email_address #pylint: disable=E1101 if mail is not None and mail not in principal_obj.get('mail', []): raise errors.ValidationError( name='csr', error=_("DN emailAddress does not match " "any of user's email addresses")) # We got this far so the principal entry exists, can we write it? if not ldap.can_write(dn, "usercertificate"): raise errors.ACIError( info=_("Insufficient 'write' privilege " "to the 'userCertificate' attribute of entry '%s'.") % dn) # Validate the subject alt name, if any for name_type, desc, name, _der_name in subjectaltname: if name_type == nss.certDNSName: name = unicode(name) alt_principal = None alt_principal_obj = None try: if principal_type == HOST: alt_principal = kerberos.Principal((u'host', name), principal.realm) alt_principal_obj = api.Command['host_show'](name, all=True) elif principal_type == SERVICE: alt_principal = kerberos.Principal( (principal.service_name, name), principal.realm) alt_principal_obj = api.Command['service_show']( alt_principal, all=True) elif principal_type == USER: raise errors.ValidationError( name='csr', error=_("subject alt name type %s is forbidden " "for user principals") % desc) except errors.NotFound: # We don't want to issue any certificates referencing # machines we don't know about. Nothing is stored in this # host record related to this certificate. raise errors.NotFound(reason=_( 'The service principal for ' 'subject alt name %s in certificate request does not ' 'exist') % name) if alt_principal_obj is not None: altdn = alt_principal_obj['result']['dn'] if not ldap.can_write(altdn, "usercertificate"): raise errors.ACIError(info=_( "Insufficient privilege to create a certificate " "with subject alt name '%s'.") % name) if alt_principal is not None and not bypass_caacl: caacl_check(principal_type, alt_principal, ca, profile_id) elif name_type in [ (nss.certOtherName, x509.SAN_UPN), (nss.certOtherName, x509.SAN_KRB5PRINCIPALNAME), ]: if name != principal_string: raise errors.ACIError( info=_("Principal '%s' in subject alt name does not " "match requested principal") % name) elif name_type == nss.certRFC822Name: if principal_type == USER: if name not in principal_obj.get('mail', []): raise errors.ValidationError( name='csr', error=_("RFC822Name does not match " "any of user's email addresses")) else: raise errors.ValidationError( name='csr', error=_("subject alt name type %s is forbidden " "for non-user principals") % desc) else: raise errors.ACIError( info=_("Subject alt name type %s is forbidden") % desc) # Request the certificate try: result = self.Backend.ra.request_certificate( csr, profile_id, ca_id, request_type=request_type) except errors.HTTPRequestError as e: if e.status == 409: # pylint: disable=no-member raise errors.CertificateOperationError( error=_("CA '%s' is disabled") % ca) else: raise e if not raw: self.obj._parse(result, all) result['request_id'] = int(result['request_id']) result['cacn'] = ca_obj['cn'][0] # Success? Then add it to the principal's entry # (unless the profile tells us not to) profile = api.Command['certprofile_show'](profile_id) store = profile['result']['ipacertprofilestoreissued'][0] == 'TRUE' if store and 'certificate' in result: cert = str(result.get('certificate')) kwargs = dict(addattr=u'usercertificate={}'.format(cert)) if principal_type == SERVICE: api.Command['service_mod'](principal_string, **kwargs) elif principal_type == HOST: api.Command['host_mod'](principal.hostname, **kwargs) elif principal_type == USER: api.Command['user_mod'](principal.username, **kwargs) return dict( result=result, value=pkey_to_value(int(result['request_id']), kw), )
def __call__(self, environ, start_response): def attempt_kinit(user_principal, password, ipa_ccache_name, use_armor=True): try: # try to remove in case an old file was there os.unlink(ipa_ccache_name) except OSError: pass try: self.kinit(user_principal, password, ipa_ccache_name, use_armor=use_armor) except PasswordExpired as e: return self.unauthorized(environ, start_response, str(e), 'password-expired') except InvalidSessionPassword as e: return self.unauthorized(environ, start_response, str(e), 'invalid-password') except KrbPrincipalExpired as e: return self.unauthorized(environ, start_response, str(e), 'krbprincipal-expired') except UserLocked as e: return self.unauthorized(environ, start_response, str(e), 'user-locked') return None logger.debug('WSGI login_password.__call__:') # Get the user and password parameters from the request content_type = environ.get('CONTENT_TYPE', '').lower() if not content_type.startswith('application/x-www-form-urlencoded'): return self.bad_request( environ, start_response, "Content-Type must be application/x-www-form-urlencoded") method = environ.get('REQUEST_METHOD', '').upper() if method == 'POST': query_string = read_input(environ) else: return self.bad_request(environ, start_response, "HTTP request method must be POST") try: query_dict = parse_qs(query_string) except Exception: return self.bad_request(environ, start_response, "cannot parse query data") user = query_dict.get('user', None) if user is not None: if len(user) == 1: user = user[0] else: return self.bad_request(environ, start_response, "more than one user parameter") else: return self.bad_request(environ, start_response, "no user specified") # allows login in the form user@SERVER_REALM or user@server_realm # we kinit as enterprise principal so we can assume that unknown realms # are UPN try: user_principal = kerberos.Principal(user) except Exception: # the principal is malformed in some way (e.g. user@REALM1@REALM2) # netbios names (NetBIOS1\user) are also not accepted (yet) return self.unauthorized(environ, start_response, '', 'denied') password = query_dict.get('password', None) if password is not None: if len(password) == 1: password = password[0] else: return self.bad_request(environ, start_response, "more than one password parameter") else: return self.bad_request(environ, start_response, "no password specified") # Get the ccache we'll use and attempt to get credentials in it with user,password ipa_ccache_name = os.path.join(paths.IPA_CCACHES, 'kinit_{}'.format(os.getpid())) try: result = attempt_kinit(user_principal, password, ipa_ccache_name, use_armor=True) except KrbPrincipalWrongFAST: result = attempt_kinit(user_principal, password, ipa_ccache_name, use_armor=False) if result is not None: return result result = self.finalize_kerberos_acquisition('login_password', ipa_ccache_name, environ, start_response) try: # Try not to litter the filesystem with unused TGTs os.unlink(ipa_ccache_name) except OSError: pass return result
def execute(self, csr, all=False, raw=False, **kw): ca_enabled_check() ldap = self.api.Backend.ldap2 add = kw.get('add') request_type = kw.get('request_type') profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE) # Check that requested authority exists (done before CA ACL # enforcement so that user gets better error message if # referencing nonexistant CA) and look up authority ID. # ca = kw['cacn'] ca_obj = api.Command.ca_show(ca)['result'] ca_id = ca_obj['ipacaid'][0] """ Access control is partially handled by the ACI titled 'Hosts can modify service userCertificate'. This is for the case where a machine binds using a host/ prinicpal. It can only do the request if the target hostname is in the managedBy attribute which is managed using the add/del member commands. Binding with a user principal one needs to be in the request_certs taskgroup (directly or indirectly via role membership). """ principal = kw.get('principal') principal_string = unicode(principal) if principal.is_user: principal_type = USER elif principal.is_host: principal_type = HOST else: principal_type = SERVICE bind_principal = kerberos.Principal(getattr(context, 'principal')) bind_principal_string = unicode(bind_principal) if bind_principal.is_user: bind_principal_type = USER elif bind_principal.is_host: bind_principal_type = HOST else: bind_principal_type = SERVICE if (bind_principal_string != principal_string and bind_principal_type != HOST): # Can the bound principal request certs for another principal? self.check_access() try: self.check_access("request certificate ignore caacl") bypass_caacl = True except errors.ACIError: bypass_caacl = False if not bypass_caacl: caacl_check(principal_type, principal, ca, profile_id) try: csr_obj = pkcs10.load_certificate_request(csr) except ValueError as e: raise errors.CertificateOperationError( error=_("Failure decoding Certificate Signing Request: %s") % e) try: ext_san = csr_obj.extensions.get_extension_for_oid( cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) except cryptography.x509.extensions.ExtensionNotFound: ext_san = None # self-service and host principals may bypass SAN permission check if (bind_principal_string != principal_string and bind_principal_type != HOST): if ext_san is not None: self.check_access('request certificate with subjectaltname') dn = None principal_obj = None # See if the service exists and punt if it doesn't and we aren't # going to add it try: 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.hostname, all=True) elif principal_type == USER: principal_obj = api.Command['user_show'](principal.username, all=True) except errors.NotFound as e: if add: if principal_type == SERVICE: principal_obj = api.Command['service_add']( principal_string, force=True) else: princtype_str = PRINCIPAL_TYPE_STRING_MAP[principal_type] raise errors.OperationNotSupportedForPrincipalType( operation=_("'add' option"), principal_type=princtype_str) else: raise errors.NotFound( reason=_("The principal for this request doesn't exist.")) principal_obj = principal_obj['result'] dn = principal_obj['dn'] # Ensure that the DN in the CSR matches the principal # # We only look at the "most specific" CN value cns = csr_obj.subject.get_attributes_for_oid( cryptography.x509.oid.NameOID.COMMON_NAME) if len(cns) == 0: raise errors.ValidationError( name='csr', error=_("No Common Name was found in subject of request.")) cn = cns[-1].value # "most specific" is end of list if principal_type in (SERVICE, HOST): 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.hostname)) elif principal_type == USER: # check user name if cn != principal.username: raise errors.ValidationError( name='csr', error=_("DN commonName does not match user's login")) # check email address # # fail if any email addr from DN does not appear in ldap entry email_addrs = csr_obj.subject.get_attributes_for_oid( cryptography.x509.oid.NameOID.EMAIL_ADDRESS) if len(set(email_addrs) - set(principal_obj.get('mail', []))) > 0: raise errors.ValidationError( name='csr', error=_("DN emailAddress does not match " "any of user's email addresses")) # We got this far so the principal entry exists, can we write it? if not ldap.can_write(dn, "usercertificate"): raise errors.ACIError( info=_("Insufficient 'write' privilege " "to the 'userCertificate' attribute of entry '%s'.") % dn) # Validate the subject alt name, if any generalnames = [] if ext_san is not None: generalnames = x509.process_othernames(ext_san.value) for gn in generalnames: if isinstance(gn, cryptography.x509.general_name.DNSName): name = gn.value alt_principal = None alt_principal_obj = None try: if principal_type == HOST: alt_principal = kerberos.Principal((u'host', name), principal.realm) alt_principal_obj = api.Command['host_show'](name, all=True) elif principal_type == SERVICE: alt_principal = kerberos.Principal( (principal.service_name, name), principal.realm) alt_principal_obj = api.Command['service_show']( alt_principal, all=True) elif principal_type == USER: raise errors.ValidationError( name='csr', error=_("subject alt name type %s is forbidden " "for user principals") % "DNSName") except errors.NotFound: # We don't want to issue any certificates referencing # machines we don't know about. Nothing is stored in this # host record related to this certificate. raise errors.NotFound(reason=_( 'The service principal for ' 'subject alt name %s in certificate request does not ' 'exist') % name) if alt_principal_obj is not None: altdn = alt_principal_obj['result']['dn'] if not ldap.can_write(altdn, "usercertificate"): raise errors.ACIError(info=_( "Insufficient privilege to create a certificate " "with subject alt name '%s'.") % name) if alt_principal is not None and not bypass_caacl: caacl_check(principal_type, alt_principal, ca, profile_id) elif isinstance(gn, (x509.KRB5PrincipalName, x509.UPN)): if gn.name != principal_string: raise errors.ACIError( info=_("Principal '%s' in subject alt name does not " "match requested principal") % gn.name) elif isinstance(gn, cryptography.x509.general_name.RFC822Name): if principal_type == USER: if gn.value not in principal_obj.get('mail', []): raise errors.ValidationError( name='csr', error=_("RFC822Name does not match " "any of user's email addresses")) else: raise errors.ValidationError( name='csr', error=_("subject alt name type %s is forbidden " "for non-user principals") % "RFC822Name") else: raise errors.ACIError( info=_("Subject alt name type %s is forbidden") % type(gn).__name__) # Request the certificate try: # re-serialise to PEM, in case the user-supplied data has # extraneous material that will cause Dogtag to freak out csr_pem = csr_obj.public_bytes(serialization.Encoding.PEM) result = self.Backend.ra.request_certificate( csr_pem, profile_id, ca_id, request_type=request_type) except errors.HTTPRequestError as e: if e.status == 409: # pylint: disable=no-member raise errors.CertificateOperationError( error=_("CA '%s' is disabled") % ca) else: raise e if not raw: self.obj._parse(result, all) result['request_id'] = int(result['request_id']) result['cacn'] = ca_obj['cn'][0] # Success? Then add it to the principal's entry # (unless the profile tells us not to) profile = api.Command['certprofile_show'](profile_id) store = profile['result']['ipacertprofilestoreissued'][0] == 'TRUE' if store and 'certificate' in result: cert = str(result.get('certificate')) kwargs = dict(addattr=u'usercertificate={}'.format(cert)) if principal_type == SERVICE: api.Command['service_mod'](principal_string, **kwargs) elif principal_type == HOST: api.Command['host_mod'](principal.hostname, **kwargs) elif principal_type == USER: api.Command['user_mod'](principal.username, **kwargs) return dict( result=result, value=pkey_to_value(int(result['request_id']), kw), )
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): assert isinstance(dn, DN) hostname = keys[0] if len(keys) == 2: netbiosname = keys[1] else: # By default take leftmost label from the host name netbiosname = DNSName.from_text(hostname)[0].decode().upper() # SMB service requires existence of the host object # because DCE RPC calls authenticated with GSSAPI are using # host/.. principal by default for validation try: hostresult = self.api.Command['host_show'](hostname)['result'] except errors.NotFound: raise errors.NotFound( reason=_("The host '%s' does not exist to add a service to.") % hostname) # We cannot afford the host not being resolvable even for # clustered environments with CTDB because the target name # has to exist even in that case util.verify_host_resolvable(hostname) smbaccount = '{name}$'.format(name=netbiosname) smbprincipal = 'cifs/{hostname}'.format(hostname=hostname) entry_attrs['krbprincipalname'] = [ str(kerberos.Principal(smbprincipal, realm=self.api.env.realm)), str(kerberos.Principal(smbaccount, realm=self.api.env.realm)) ] entry_attrs['krbcanonicalname'] = entry_attrs['krbprincipalname'][0] # Rewrite DN using proper rdn and new canonical name because when # LDAPCreate.execute() was called, it set DN to krbcanonicalname=$value dn = DN(('krbprincipalname', entry_attrs['krbcanonicalname']), DN(self.obj.container_dn, api.env.basedn)) # Enforce ipaKrbPrincipalAlias to aid case-insensitive searches as # krbPrincipalName/krbCanonicalName are case-sensitive in Kerberos # schema entry_attrs['ipakrbprincipalalias'] = entry_attrs['krbcanonicalname'] for o in ('ipakrbprincipal', 'ipaidobject', 'krbprincipalaux', 'posixaccount'): if o not in entry_attrs['objectclass']: entry_attrs['objectclass'].append(o) entry_attrs['uid'] = [ '/'.join(kerberos.Principal(smbprincipal).components) ] entry_attrs['uid'].append(smbaccount) entry_attrs['cn'] = netbiosname entry_attrs['homeDirectory'] = '/dev/null' entry_attrs['uidNumber'] = DNA_MAGIC entry_attrs['gidNumber'] = DNA_MAGIC self.obj.validate_ipakrbauthzdata(entry_attrs) if 'managedby' not in entry_attrs: entry_attrs['managedby'] = hostresult['dn'] update_krbticketflags(ldap, entry_attrs, attrs_list, options, False) return dn