Ejemplo n.º 1
0
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"',
        ),
        Certificate('usercertificate*',
            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
Ejemplo n.º 2
0
class idoverrideuser(baseidoverride):

    object_name = _('User ID override')
    object_name_plural = _('User ID overrides')

    label = _('User ID overrides')
    label_singular = _('User ID override')
    allow_rename = True

    # ID user overrides are bindable because we map SASL GSSAPI
    # authentication of trusted users to ID user overrides in the
    # default trust view.
    bindable = True

    permission_filter_objectclasses = ['ipaUserOverride']
    managed_permissions = {
        'System: Read User ID Overrides': {
            'ipapermbindruletype': 'all',
            'ipapermright': {'read', 'search', 'compare'},
            'ipapermdefaultattr': {
                'objectClass', 'ipaAnchorUUID', 'uidNumber', 'description',
                'homeDirectory', 'uid', 'ipaOriginalUid', 'loginShell',
                'gecos', 'gidNumber', 'ipaSshPubkey', 'usercertificate'
            },
        },
    }

    object_class = baseidoverride.object_class + ['ipaUserOverride']
    possible_objectclasses = ['ipasshuser', 'ipaSshGroupOfPubKeys']
    default_attributes = baseidoverride.default_attributes + [
        'homeDirectory',
        'uidNumber',
        'uid',
        'ipaOriginalUid',
        'loginShell',
        'ipaSshPubkey',
        'gidNumber',
        'gecos',
        'usercertificate;binary',
    ]

    search_display_attributes = baseidoverride.default_attributes + [
        'homeDirectory',
        'uidNumber',
        'uid',
        'ipaOriginalUid',
        'loginShell',
        'ipaSshPubkey',
        'gidNumber',
        'gecos',
    ]

    takes_params = baseidoverride.takes_params + (
        Str(
            'uid?',
            pattern=PATTERN_GROUPUSER_NAME,
            pattern_errmsg='may only include letters, numbers, _, -, . and $',
            maxlength=255,
            cli_name='login',
            label=_('User login'),
            normalizer=lambda value: value.lower(),
        ),
        Int(
            'uidnumber?',
            cli_name='uid',
            label=_('UID'),
            doc=_('User ID Number'),
            minvalue=1,
        ),
        Str(
            'gecos?',
            label=_('GECOS'),
        ),
        Int(
            'gidnumber?',
            label=_('GID'),
            doc=_('Group ID Number'),
            minvalue=1,
        ),
        Str(
            'homedirectory?',
            cli_name='homedir',
            label=_('Home directory'),
        ),
        Str(
            'loginshell?',
            cli_name='shell',
            label=_('Login shell'),
        ),
        Str('ipaoriginaluid?', flags=['no_option', 'no_output']),
        Str(
            'ipasshpubkey*',
            validate_sshpubkey,
            cli_name='sshpubkey',
            label=_('SSH public key'),
            normalizer=normalize_sshpubkey,
            flags=['no_search'],
        ),
        Certificate(
            'usercertificate*',
            cli_name='certificate',
            label=_('Certificate'),
            doc=_('Base-64 encoded user certificate'),
            flags=[
                'no_search',
            ],
        ),
    )

    override_object = 'user'

    def update_original_uid_reference(self, entry_attrs):
        anchor = entry_attrs.single_value['ipaanchoruuid']
        try:
            original_uid = resolve_anchor_to_object_name(
                self.backend, self.override_object, anchor)
            entry_attrs['ipaOriginalUid'] = original_uid

        except (errors.NotFound, errors.ValidationError):
            # Anchor could not be resolved, this means we had to specify the
            # object to manipulate using a raw anchor value already, hence
            # we have no way to update the original_uid
            pass

    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')
Ejemplo n.º 3
0
class service(LDAPObject):
    """
    Service object.
    """
    container_dn = api.env.container_service
    object_name = _('service')
    object_name_plural = _('services')
    object_class = [
        'krbprincipal', 'krbprincipalaux', 'krbticketpolicyaux', 'ipaobject',
        'ipaservice', 'pkiuser'
    ]
    possible_objectclasses = ['ipakrbprincipal', 'ipaallowedoperations']
    permission_filter_objectclasses = ['ipaservice']
    search_attributes = ['krbprincipalname', 'managedby', 'ipakrbauthzdata']
    default_attributes = [
        'krbprincipalname', 'krbcanonicalname', 'usercertificate', 'managedby',
        'ipakrbauthzdata', 'memberof', 'ipaallowedtoperform',
        'krbprincipalauthind']
    uuid_attribute = 'ipauniqueid'
    attribute_members = {
        'managedby': ['host'],
        'memberof': ['role'],
        'ipaallowedtoperform_read_keys': ['user', 'group', 'host', 'hostgroup'],
        'ipaallowedtoperform_write_keys': ['user', 'group', 'host', 'hostgroup'],
    }
    bindable = True
    relationships = {
        'managedby': ('Managed by', 'man_by_', 'not_man_by_'),
        'ipaallowedtoperform_read_keys': ('Allow to retrieve keytab by', 'retrieve_keytab_by_', 'not_retrieve_keytab_by_'),
        'ipaallowedtoperform_write_keys': ('Allow to create keytab by', 'write_keytab_by_', 'not_write_keytab_by'),
    }
    password_attributes = [('krbprincipalkey', 'has_keytab')]
    managed_permissions = {
        'System: Read Services': {
            'replaces_global_anonymous_aci': True,
            'ipapermbindruletype': 'all',
            'ipapermright': {'read', 'search', 'compare'},
            'ipapermdefaultattr': {
                'objectclass',
                'ipauniqueid', 'managedby', 'memberof', 'usercertificate',
                'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases',
                'krbprincipalexpiration', 'krbpasswordexpiration',
                'krblastpwdchange', 'ipakrbauthzdata', 'ipakrbprincipalalias',
                'krbobjectreferences', 'krbprincipalauthind',
            },
        },
        'System: Add Services': {
            'ipapermright': {'add'},
            'replaces': [
                '(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Services";allow (add) groupdn = "ldap:///cn=Add Services,cn=permissions,cn=pbac,$SUFFIX";)',
            ],
            'default_privileges': {'Service Administrators'},
        },
        'System: Manage Service Keytab': {
            'ipapermright': {'write'},
            'ipapermdefaultattr': {'krblastpwdchange', 'krbprincipalkey'},
            'replaces': [
                '(targetattr = "krbprincipalkey || krblastpwdchange")(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Manage service keytab";allow (write) groupdn = "ldap:///cn=Manage service keytab,cn=permissions,cn=pbac,$SUFFIX";)',
            ],
            'default_privileges': {'Service Administrators', 'Host Administrators'},
        },
        'System: Manage Service Keytab Permissions': {
            'ipapermright': {'read', 'search', 'compare', 'write'},
            'ipapermdefaultattr': {
                'ipaallowedtoperform;write_keys',
                'ipaallowedtoperform;read_keys', 'objectclass'
            },
            'default_privileges': {'Service Administrators', 'Host Administrators'},
        },
        'System: Modify Services': {
            'ipapermright': {'write'},
            'ipapermdefaultattr': {'usercertificate', 'krbprincipalauthind'},
            'replaces': [
                '(targetattr = "usercertificate")(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Services";allow (write) groupdn = "ldap:///cn=Modify Services,cn=permissions,cn=pbac,$SUFFIX";)',
            ],
            'default_privileges': {'Service Administrators'},
        },
        'System: Manage Service Principals': {
            'ipapermright': {'write'},
            'ipapermdefaultattr': {'krbprincipalname', 'krbcanonicalname'},
            'default_privileges': {
                'Service Administrators',
            },
        },
        'System: Remove Services': {
            'ipapermright': {'delete'},
            'replaces': [
                '(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Services";allow (delete) groupdn = "ldap:///cn=Remove Services,cn=permissions,cn=pbac,$SUFFIX";)',
            ],
            'default_privileges': {'Service Administrators'},
        },
    }

    label = _('Services')
    label_singular = _('Service')

    takes_params = (
        Principal(
            'krbcanonicalname',
            validate_realm,
            cli_name='canonical_principal',
            label=_('Principal name'),
            doc=_('Service principal'),
            primary_key=True,
            normalizer=normalize_principal,
            require_service=True
        ),
        Principal(
            'krbprincipalname*',
            validate_realm,
            cli_name='principal',
            label=_('Principal alias'),
            doc=_('Service principal alias'),
            normalizer=normalize_principal,
            require_service=True,
            flags={'no_create'}
        ),
        Certificate('usercertificate*',
            cli_name='certificate',
            label=_('Certificate'),
            doc=_('Base-64 encoded service certificate'),
            flags=['no_search',],
        ),
        Str('subject',
            label=_('Subject'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('serial_number',
            label=_('Serial Number'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('serial_number_hex',
            label=_('Serial Number (hex)'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('issuer',
            label=_('Issuer'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('valid_not_before',
            label=_('Not Before'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('valid_not_after',
            label=_('Not After'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('sha1_fingerprint',
            label=_('Fingerprint (SHA1)'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('sha256_fingerprint',
            label=_('Fingerprint (SHA256)'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        Str('revocation_reason?',
            label=_('Revocation reason'),
            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
        ),
        StrEnum('ipakrbauthzdata*',
            cli_name='pac_type',
            label=_('PAC type'),
            doc=_("Override default list of supported PAC types."
                  " Use 'NONE' to disable PAC support for this service,"
                  " e.g. this might be necessary for NFS services."),
            values=(u'MS-PAC', u'PAD', u'NONE'),
        ),
        Str('krbprincipalauthind*',
            cli_name='auth_ind',
            label=_('Authentication Indicators'),
            doc=_("Defines a whitelist for Authentication Indicators."
                  " Use 'otp' to allow OTP-based 2FA authentications."
                  " Use 'radius' to allow RADIUS-based 2FA authentications."
                  " Other values may be used for custom configurations."),
        ),
    ) + ticket_flags_params

    def validate_ipakrbauthzdata(self, entry):
        new_value = entry.get('ipakrbauthzdata', [])

        if not new_value:
            return

        if not isinstance(new_value, (list, tuple)):
            new_value = set([new_value])
        else:
            new_value = set(new_value)

        if u'NONE' in new_value and len(new_value) > 1:
            raise errors.ValidationError(name='ipakrbauthzdata',
                error=_('NONE value cannot be combined with other PAC types'))

    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 get_primary_key_from_dn(self, dn):
        """
        If the entry has krbcanonicalname set return the value of the
        attribute. If the attribute is not found, assume old-style entry which
        should have only single value of krbprincipalname and return it.

        Otherwise return input DN.
        """
        assert isinstance(dn, DN)

        try:
            entry_attrs = self.backend.get_entry(
                dn, [self.primary_key.name]
            )
            try:
                return entry_attrs[self.primary_key.name][0]
            except (KeyError, IndexError):
                return ''
        except errors.NotFound:
            pass

        try:
            return dn['krbprincipalname']
        except KeyError:
            return unicode(dn)

    def populate_krbcanonicalname(self, entry_attrs, options):
        if options.get('raw', False):
            return

        entry_attrs.setdefault(
            'krbcanonicalname', entry_attrs['krbprincipalname'])
Ejemplo n.º 4
0
class ModCertMapData(LDAPModAttribute):
    attribute = 'ipacertmapdata'
    takes_options = (
        DNParam('issuer?',
                cli_name='issuer',
                label=_('Issuer'),
                doc=_('Issuer of the certificate'),
                flags=['virtual_attribute']),
        DNParam('subject?',
                cli_name='subject',
                label=_('Subject'),
                doc=_('Subject of the certificate'),
                flags=['virtual_attribute']),
        Certificate('certificate*',
                    cli_name='certificate',
                    label=_('Certificate'),
                    doc=_('Base-64 encoded user certificate'),
                    flags=['virtual_attribute']),
    )

    @staticmethod
    def _build_mapdata(subject, issuer):
        return u'X509:<I>{issuer}<S>{subject}'.format(
            issuer=issuer.x500_text(), subject=subject.x500_text())

    @classmethod
    def _convert_options_to_certmap(cls,
                                    entry_attrs,
                                    issuer=None,
                                    subject=None,
                                    certificates=()):
        """
        Converts options to ipacertmapdata

        When --subject --issuer or --certificate options are used,
        the value for ipacertmapdata is built from extracting subject and
        issuer,
        converting their values to X500 ordering and using the format
        X509:<I>issuer<S>subject
        For instance:
        X509:<I>O=DOMAIN,CN=Certificate Authority<S>O=DOMAIN,CN=user
        A list of values can be returned if --certificate is used multiple
        times, or in conjunction with --subject --issuer.
        """
        data = []
        data.extend(entry_attrs.get(cls.attribute, list()))

        if issuer or subject:
            data.append(cls._build_mapdata(subject, issuer))

        for cert in certificates:
            issuer = DN(cert.issuer)
            subject = DN(cert.subject)
            if not subject:
                raise errors.ValidationError(
                    name='certificate',
                    error=_('cannot have an empty subject'))
            data.append(cls._build_mapdata(subject, issuer))

        entry_attrs[cls.attribute] = data

    def get_args(self):
        # ipacertmapdata is not mandatory as it can be built
        # from the values subject+issuer or from reading certificate
        for arg in super(ModCertMapData, self).get_args():
            if arg.name == 'ipacertmapdata':
                yield arg.clone(required=False, alwaysask=False)
            else:
                yield arg.clone()

    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys,
                     **options):
        # The 3 valid calls are
        # ipa user-add-certmapdata LOGIN --subject xx --issuer yy
        # ipa user-add-certmapdata LOGIN [DATA] --certificate xx
        # ipa user-add-certmapdata LOGIN DATA
        # Check that at least one of the 3 formats is used

        try:
            certmapdatas = keys[1] or []
        except IndexError:
            certmapdatas = []
        issuer = options.get('issuer')
        subject = options.get('subject')
        certificates = options.get('certificate', [])

        # If only LOGIN is supplied, then we need either subject or issuer or
        # certificate
        if (not certmapdatas and not issuer and not subject
                and not certificates):
            raise errors.RequirementError(name='ipacertmapdata')

        # If subject or issuer is provided, other options are not allowed
        if subject or issuer:
            if certificates:
                raise errors.MutuallyExclusiveError(
                    reason=_('cannot specify both subject/issuer '
                             'and certificate'))
            if certmapdatas:
                raise errors.MutuallyExclusiveError(
                    reason=_('cannot specify both subject/issuer '
                             'and ipacertmapdata'))
            # If subject or issuer is provided, then the other one is required
            if not subject:
                raise errors.RequirementError(name='subject')
            if not issuer:
                raise errors.RequirementError(name='issuer')

        # if the command is called with --subject --issuer or --certificate
        # we need to add ipacertmapdata to the attrs_list in order to
        # display the resulting value in the command output
        if 'ipacertmapdata' not in attrs_list:
            attrs_list.append('ipacertmapdata')

        self._convert_options_to_certmap(entry_attrs,
                                         issuer=issuer,
                                         subject=subject,
                                         certificates=certificates)

        return dn