#8187 IPA API, *_mod commands, with rename support
Opened by schlitzered. Modified

Hi,

i am trying to build a terraform provider using the api doc.

i suspect that the "rename" option, on some "_mod" commands relates to one of the "takes_args" arguments, which have "primary_key" set to true.

for most cases, this seems pretty obvious, because there is only one "argument", which has "primary_key" set to true.

but there are also cases, where there are two args, with "primary_key" set to true.

i think that in those cases, the second argument, is the one that can be changed, is my assumption right?

Kind Regards,


No, rename is an option. The value to the option is the rename value.

You can add -vvv to an ipa command to see how the cli tool passes options into the API. For a user renaming from tuser1 to tuser this looks like:

{
    "id": 0,
    "method": "user_mod/1",
    "params": [
        [
            "tuser1"
        ],
        {
            "rename": "tuser",
            "version": "2.235"
        }
    ]
}

ipa -vvv user-mod --rename tuser tuser1

thx for the reply,

i am aware that "rename" is an option (except for aci, where it is a command "aci_rename" or this is something completely different).

what i am asking is, when i specify a value for rename. what exactly gets renamed?

in case of "user_mod", the rename option will change the "uid", which is an argument, but not cn, which is an option. the uid has "primary_key" set to true.

in case of "group_mod" the rename option will change the "cn", which in this case is an argument. the cn has "primary_key" set to true.

but now we also have things like:
- automountkey_mod
- dnsrecord_mod
- idoverridegroup_mod
- idoverrideuser_mod

which have two arguments, and both have primary_key set to true. i guess that one of the both, is the one that gets changed be there respective "rename" option, but which one?

btw, i am talking about the api spec you get via this:

$ kinit username
$ curl --negotiate -u : https://$(hostname)/ipa/session/json \
-H "Content-Type: application/json" \
-H "Referer: https://$(hostname)/ipa" \
-d '{"method":"json_metadata","params":[[],{"command":"all"}]}'

I'd have to dig into the plugins I think. It is a bit of an anti-pattern because these objects are generally have multiple parts to create a unique DN (aci is just an oddball since it isn't a DN at all).

Automount keys have a location and a map, A dnsrecord has a zone. So IIRC the "key" for these is really an option or options and not one of the args. So these args are primary keys in a way because they are part of the structure of the DN to make it unique, they are just not in themselves unique. Why I made the automount key an option and not an argument I don't remember, but I suspect that model was extended into dnsrecord and idoverride*. I believe an option is also marked as a primary key.

What is it you're trying to do?

my plan is to write a terraform plugin for freeipa/redhat IdM.

terrafrom itself is an infrastructure as code tool.

and since IPA/IDM has so many commands, i am currently writing a parser for the API spec, which in the end will auto create the "go" code for me, that is needed for this plugin.

but i need to know how to handle all the different corner cases.

so the basic questions i have right now is, what field gets renamed by the different rename options.

Kind Regards.

I'd suggest you to look at the source code. ipaserver/plugins/baseldap.py is the place where all base logic is defined for LDAP-backed classes. If an object (LDAPObject-derived) has allow_rename = True, then it will expose rename option in its update path (derived from LDAPUpdate class). Each LDAPObject-derived class has own primary key.

The base logic for rename is in LDAPUpdate.execute(). When primary key is part of the attributes asked to be updated, a new DN is created by replacing RDN of the existing object with a new RDN from the primary key's value in the attribute set provided by the command and then an entry is moved to the new DN using LDAP RENAME operation.

Specifically, the core part of the code that executes rename looks like this:

                    new_dn = DN((self.obj.primary_key.name,
                                 entry_attrs[self.obj.primary_key.name]),
                                *entry_attrs.dn[1:])
                    self._exc_wrapper(keys, options, ldap.move_entry)(
                        entry_attrs.dn,
                        new_dn)
                    rdnkeys = (keys[:-1] +
                               (entry_attrs[self.obj.primary_key.name], ))
                    entry_attrs.dn = self.obj.get_dn(*rdnkeys)

So the field you are interested in is always a primary key. Unfortunately, the information about it is not returned in the metadata explicitly, it seems.

thanks for the info!

okay, i guess for now, i will simply not implement "rename" for the _mod commands that have two primary keys, and only implement it for the ones that have a single primary_key.

works for me, since these objects are rather "exotic" and I have no direct use case for them ;-)

but it might makes sense, to extend the API doc, to at least the tell in the "doc" attribute of "rename" to tell which primary key will be changed? i guess this would be fairly easy to implement, and you do not have to introduce another attribute to the api doc?

There is already (kind of) support to handle primary keys in the output of commands, they just not named explicitly. For example, value is the output of a command is almost always a primary key:

$ ipa output-find user-add --raw
  name: summary
  doc: User-friendly description of action performed
  type: str
  required: False
  name: result
  type: dict
  name: value
  doc: The primary_key value of the entry, e.g. 'jdoe' for a user
  type: str
----------------------------
Number of entries returned 3
----------------------------

So it can be extended to provide one.

yeah, but this is not that machine friendly :-/

i mean, i could manually curate this information in my code, but i would like to avoid this. IMHO, the API spec should deliver this information in a machine friendly way.

It is coming through the same schema end point.

ipa: INFO: Request: {
    "id": 0,
    "method": "output_find/1",
    "params": [
        [
            "user-add"
        ],
        {
            "raw": true,
            "version": "2.235"
        }
    ]
}
...
ipa: INFO: Response: {
    "error": null,
    "id": 0,
    "principal": "principal@SOME.REALM",
    "result": {
        "count": 3,
        "result": [
            {
                "doc": "User-friendly description of action performed",
                "name": "summary",
                "required": false,
                "type": "str"
            },
            {
                "name": "result",
                "type": "dict"
            },
            {
                "doc": "The primary_key value of the entry, e.g. 'jdoe' for a user",
                "name": "value",
                "type": "str"
            }
        ],
        "summary": null,
        "truncated": false
    },
    "version": "4.8.4"
}

i am sorry, maybe i am just not seeing it, but where in the output is mentioned, that the primary key for "user-add" is "uid"?

Adding a primary_key attribute to ipa param-find user output would probably be enough, btw. Because we can get that from the params (primary_key is set on a param that is the primary key).

Here is a quick patch to test

freeipa-expose-primary_key-param.patch

# ipa -e in_server=True console
(Custom IPA interactive Python console)
    api: IPA API object
    pp: pretty printer
>>> api.Command.param_find('user')
{'result': [{'name': 'uid', 'type': 'str', 'primary_key': True, 'label': 'User login'}, {'name': 'givenname', 'type': 'str', 'label': 'First name'}, {'name': 'sn', 'type': 'str', 'label': 'Last name'}, {'name': 'cn', 'type': 'str', 'label': 'Full name'}, {'name': 'displayname', 'type': 'str', 'required': False, 'label': 'Display name'}, {'name': 'initials', 'type': 'str', 'required': False, 'label': 'Initials'}, {'name': 'homedirectory', 'type': 'str', 'required': False, 'label': 'Home directory'}, {'name': 'gecos', 'type': 'str', 'required': False, 'label': 'GECOS'}, {'name': 'loginshell', 'type': 'str', 'required': False, 'label': 'Login shell'}, {'name': 'krbcanonicalname', 'type': 'Principal', 'required': False, 'label': 'Principal name'}, {'name': 'krbprincipalname', 'type': 'Principal', 'required': False, 'multivalue': True, 'label': 'Principal alias'}, {'name': 'krbprincipalexpiration', 'type': 'datetime', 'required': False, 'label': 'Kerberos principal expiration'}, {'name': 'krbpasswordexpiration', 'type': 'datetime', 'required': False, 'label': 'User password expiration'}, {'name': 'mail', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Email address'}, {'name': 'userpassword', 'type': 'str', 'required': False, 'sensitive': True, 'label': 'Password', 'doc': 'Prompt to set the user password', 'exclude': ['webui']}, {'name': 'random', 'type': 'bool', 'required': False, 'doc': 'Generate a random user password'}, {'name': 'randompassword', 'type': 'str', 'required': False, 'label': 'Random password'}, {'name': 'uidnumber', 'type': 'int', 'required': False, 'label': 'UID', 'doc': 'User ID Number (system will assign one if not provided)'}, {'name': 'gidnumber', 'type': 'int', 'required': False, 'label': 'GID', 'doc': 'Group ID Number'}, {'name': 'street', 'type': 'str', 'required': False, 'label': 'Street address'}, {'name': 'l', 'type': 'str', 'required': False, 'label': 'City'}, {'name': 'st', 'type': 'str', 'required': False, 'label': 'State/Province'}, {'name': 'postalcode', 'type': 'str', 'required': False, 'label': 'ZIP'}, {'name': 'telephonenumber', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Telephone Number'}, {'name': 'mobile', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Mobile Telephone Number'}, {'name': 'pager', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Pager Number'}, {'name': 'facsimiletelephonenumber', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Fax Number'}, {'name': 'ou', 'type': 'str', 'required': False, 'label': 'Org. Unit'}, {'name': 'title', 'type': 'str', 'required': False, 'label': 'Job Title'}, {'name': 'manager', 'type': 'str', 'required': False, 'label': 'Manager'}, {'name': 'carlicense', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Car License'}, {'name': 'ipasshpubkey', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'SSH public key'}, {'name': 'sshpubkeyfp', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'SSH public key fingerprint'}, {'name': 'ipauserauthtype', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'User authentication types', 'doc': 'Types of supported user authentication'}, {'name': 'userclass', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Class', 'doc': 'User category (semantics placed on this attribute are for local interpretation)'}, {'name': 'ipatokenradiusconfiglink', 'type': 'str', 'required': False, 'label': 'RADIUS proxy configuration'}, {'name': 'ipatokenradiususername', 'type': 'str', 'required': False, 'label': 'RADIUS proxy username'}, {'name': 'departmentnumber', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Department Number'}, {'name': 'employeenumber', 'type': 'str', 'required': False, 'label': 'Employee Number'}, {'name': 'employeetype', 'type': 'str', 'required': False, 'label': 'Employee Type'}, {'name': 'preferredlanguage', 'type': 'str', 'required': False, 'label': 'Preferred Language'}, {'name': 'usercertificate', 'type': 'Certificate', 'required': False, 'multivalue': True, 'label': 'Certificate', 'doc': 'Base-64 encoded user certificate'}, {'name': 'ipacertmapdata', 'type': 'str', 'required': False, 'multivalue': True, 'label': 'Certificate mapping data', 'doc': 'Certificate mapping data'}, {'name': 'ipantlogonscript', 'type': 'str', 'required': False, 'label': 'SMB logon script path'}, {'name': 'ipantprofilepath', 'type': 'str', 'required': False, 'label': 'SMB profile path'}, {'name': 'ipanthomedirectory', 'type': 'str', 'required': False, 'label': 'SMB Home Directory'}, {'name': 'ipanthomedirectoryrive', 'type': 'str', 'required': False, 'label': 'SMB Home Directory Drive'}, {'name': 'nsaccountlock', 'type': 'bool', 'required': False, 'label': 'Account disabled'}, {'name': 'preserved', 'type': 'bool', 'required': False, 'label': 'Preserved user'}, {'name': 'inetuserstatus', 'type': 'str', 'required': False, 'label': 'User status'}, {'name': 'has_password', 'type': 'bool', 'label': 'Password'}, {'name': 'memberof_group', 'type': 'str', 'required': False, 'label': 'Member of groups'}, {'name': 'memberof_role', 'type': 'str', 'required': False, 'label': 'Roles'}, {'name': 'memberof_netgroup', 'type': 'str', 'required': False, 'label': 'Member of netgroups'}, {'name': 'memberof_sudorule', 'type': 'str', 'required': False, 'label': 'Member of Sudo rule'}, {'name': 'memberof_hbacrule', 'type': 'str', 'required': False, 'label': 'Member of HBAC rule'}, {'name': 'memberofindirect_group', 'type': 'str', 'required': False, 'label': 'Indirect Member of group'}, {'name': 'memberofindirect_netgroup', 'type': 'str', 'required': False, 'label': 'Indirect Member of netgroup'}, {'name': 'memberofindirect_role', 'type': 'str', 'required': False, 'label': 'Indirect Member of role'}, {'name': 'memberofindirect_sudorule', 'type': 'str', 'required': False, 'label': 'Indirect Member of Sudo rule'}, {'name': 'memberofindirect_hbacrule', 'type': 'str', 'required': False, 'label': 'Indirect Member of HBAC rule'}, {'name': 'has_keytab', 'type': 'bool', 'label': 'Kerberos keys available'}], 'count': 62, 'truncated': False, 'messages': [{'type': 'warning', 'name': 'VersionMissing', 'message': "API Version number was not sent, forward compatibility not guaranteed. Assuming server's API version, 2.235", 'code': 13001, 'data': {'server_version': '2.235'}}], 'summary': None}
>>> 

nice, will test this over the weekend, thx!

Note that user isn't the best object to test with since it's the simple case. automountkey or dnsrecord are better things since both have multiple primary keys.

Right. Here is the full list for all api.Object members registered at the server side (including whatever external plugins I had installed at this test deployment):

>>> from pprint import pprint
>>> pprint({t.name: [*filter(lambda x: 'primary_key' in x, api.Command.param_find(t.name)['result'])] for t in api.Object})
{'aci': [{'label': 'ACI name',
          'name': 'aciname',
          'primary_key': True,
          'type': 'str'}],
 'automember': [{'doc': 'Automember Rule',
                 'label': 'Automember Rule',
                 'name': 'cn',
                 'primary_key': True,
                 'type': 'str'}],
 'automember_default_group': [],
 'automember_task': [],
 'automountkey': [{'exclude': ['webui', 'cli'],
                   'label': 'description',
                   'name': 'description',
                   'primary_key': True,
                   'required': False,
                   'type': 'str'}],
 'automountlocation': [{'doc': 'Automount location name.',
                        'label': 'Location',
                        'name': 'cn',
                        'primary_key': True,
                        'type': 'str'}],
 'automountmap': [{'doc': 'Automount map name.',
                   'label': 'Map',
                   'name': 'automountmapname',
                   'primary_key': True,
                   'type': 'str'}],
 'ca': [{'doc': 'Name for referencing the CA',
         'label': 'Name',
         'name': 'cn',
         'primary_key': True,
         'type': 'str'}],
 'caacl': [{'label': 'ACL name',
            'name': 'cn',
            'primary_key': True,
            'type': 'str'}],
 'cert': [{'doc': 'Serial number in decimal or if prefixed with 0x in '
                  'hexadecimal',
           'label': 'Serial number',
           'name': 'serial_number',
           'primary_key': True,
           'type': 'int'}],
 'certmap': [],
 'certmapconfig': [],
 'certmaprule': [{'doc': 'Certificate Identity Mapping Rule name',
                  'label': 'Rule name',
                  'name': 'cn',
                  'primary_key': True,
                  'type': 'str'}],
 'certprofile': [{'doc': 'Profile ID for referring to this profile',
                  'label': 'Profile ID',
                  'name': 'cn',
                  'primary_key': True,
                  'type': 'str'}],
 'certreq': [{'exclude': ['cli', 'webui'],
              'label': 'Request id',
              'name': 'request_id',
              'primary_key': True,
              'type': 'int'}],
 'class': [{'label': 'Full name',
            'name': 'full_name',
            'primary_key': True,
            'type': 'str'}],
 'command': [{'label': 'Full name',
              'name': 'full_name',
              'primary_key': True,
              'type': 'str'}],
 'config': [],
 'cosentry': [{'name': 'cn', 'primary_key': True, 'type': 'str'}],
 'delegation': [{'doc': 'Delegation name',
                 'label': 'Delegation name',
                 'name': 'aciname',
                 'primary_key': True,
                 'type': 'str'}],
 'deskprofile': [{'label': 'Profile name',
                  'name': 'cn',
                  'primary_key': True,
                  'type': 'str'}],
 'deskprofileconfig': [],
 'deskprofilerule': [{'label': 'Rule name',
                      'name': 'cn',
                      'primary_key': True,
                      'type': 'str'}],
 'dns_system_records': [],
 'dnsa6record': [],
 'dnsaaaarecord': [],
 'dnsafsdbrecord': [],
 'dnsaplrecord': [],
 'dnsarecord': [],
 'dnscertrecord': [],
 'dnscnamerecord': [],
 'dnsconfig': [],
 'dnsdhcidrecord': [],
 'dnsdlvrecord': [],
 'dnsdnamerecord': [],
 'dnsdsrecord': [],
 'dnsforwardzone': [{'doc': 'Zone name (FQDN)',
                     'label': 'Zone name',
                     'name': 'idnsname',
                     'primary_key': True,
                     'type': 'DNSName'}],
 'dnshiprecord': [],
 'dnsipseckeyrecord': [],
 'dnskeyrecord': [],
 'dnskxrecord': [],
 'dnslocrecord': [],
 'dnsmxrecord': [],
 'dnsnaptrrecord': [],
 'dnsnsecrecord': [],
 'dnsnsrecord': [],
 'dnsptrrecord': [],
 'dnsrecord': [{'doc': 'Record name',
                'label': 'Record name',
                'name': 'idnsname',
                'primary_key': True,
                'type': 'DNSName'}],
 'dnsrprecord': [],
 'dnsrrsigrecord': [],
 'dnsserver': [{'doc': 'DNS Server name',
                'label': 'Server name',
                'name': 'idnsserverid',
                'primary_key': True,
                'type': 'str'}],
 'dnssigrecord': [],
 'dnsspfrecord': [],
 'dnssrvrecord': [],
 'dnssshfprecord': [],
 'dnstlsarecord': [],
 'dnstxtrecord': [],
 'dnsurirecord': [],
 'dnszone': [{'doc': 'Zone name (FQDN)',
              'label': 'Zone name',
              'name': 'idnsname',
              'primary_key': True,
              'type': 'DNSName'}],
 'group': [{'label': 'Group name',
            'name': 'cn',
            'primary_key': True,
            'type': 'str'}],
 'hbacrule': [{'label': 'Rule name',
               'name': 'cn',
               'primary_key': True,
               'type': 'str'}],
 'hbacsvc': [{'doc': 'HBAC service',
              'label': 'Service name',
              'name': 'cn',
              'primary_key': True,
              'type': 'str'}],
 'hbacsvcgroup': [{'label': 'Service group name',
                   'name': 'cn',
                   'primary_key': True,
                   'type': 'str'}],
 'host': [{'label': 'Host name',
           'name': 'fqdn',
           'primary_key': True,
           'type': 'str'}],
 'hostgroup': [{'doc': 'Name of host-group',
                'label': 'Host-group',
                'name': 'cn',
                'primary_key': True,
                'type': 'str'}],
 'idoverridegroup': [{'label': 'Anchor to override',
                      'name': 'ipaanchoruuid',
                      'primary_key': True,
                      'type': 'str'}],
 'idoverrideuser': [{'label': 'Anchor to override',
                     'name': 'ipaanchoruuid',
                     'primary_key': True,
                     'type': 'str'}],
 'idrange': [{'label': 'Range name',
              'name': 'cn',
              'primary_key': True,
              'type': 'str'}],
 'idview': [{'label': 'ID View Name',
             'name': 'cn',
             'primary_key': True,
             'type': 'str'}],
 'krbtpolicy': [{'doc': 'Manage ticket policy for specific user',
                 'label': 'User name',
                 'name': 'uid',
                 'primary_key': True,
                 'required': False,
                 'type': 'str'}],
 'location': [{'doc': 'IPA location name',
               'label': 'Location name',
               'name': 'idnsname',
               'primary_key': True,
               'type': 'DNSName'}],
 'metaobject': [{'label': 'Full name',
                 'name': 'full_name',
                 'primary_key': True,
                 'type': 'str'}],
 'netgroup': [{'label': 'Netgroup name',
               'name': 'cn',
               'primary_key': True,
               'type': 'str'}],
 'otpconfig': [],
 'otptoken': [{'label': 'Unique ID',
               'name': 'ipatokenuniqueid',
               'primary_key': True,
               'type': 'str'}],
 'output': [{'label': 'Name',
             'name': 'name',
             'primary_key': True,
             'type': 'str'}],
 'param': [{'label': 'Name',
            'name': 'name',
            'primary_key': True,
            'type': 'str'}],
 'permission': [{'label': 'Permission name',
                 'name': 'cn',
                 'primary_key': True,
                 'type': 'str'}],
 'pkinit': [],
 'privilege': [{'label': 'Privilege name',
                'name': 'cn',
                'primary_key': True,
                'type': 'str'}],
 'pwpolicy': [{'doc': 'Manage password policy for specific group',
               'label': 'Group',
               'name': 'cn',
               'primary_key': True,
               'required': False,
               'type': 'str'}],
 'radiusproxy': [{'label': 'RADIUS proxy server name',
                  'name': 'cn',
                  'primary_key': True,
                  'type': 'str'}],
 'realmdomains': [],
 'role': [{'label': 'Role name',
           'name': 'cn',
           'primary_key': True,
           'type': 'str'}],
 'selfservice': [{'doc': 'Self-service name',
                  'label': 'Self-service name',
                  'name': 'aciname',
                  'primary_key': True,
                  'type': 'str'}],
 'selinuxusermap': [{'label': 'Rule name',
                     'name': 'cn',
                     'primary_key': True,
                     'type': 'str'}],
 'server': [{'doc': 'IPA server hostname',
             'label': 'Server name',
             'name': 'cn',
             'primary_key': True,
             'type': 'str'}],
 'server_role': [],
 'service': [{'doc': 'Service principal',
              'label': 'Principal name',
              'name': 'krbcanonicalname',
              'primary_key': True,
              'type': 'Principal'}],
 'servicedelegationrule': [{'label': 'Delegation name',
                            'name': 'cn',
                            'primary_key': True,
                            'type': 'str'}],
 'servicedelegationtarget': [{'label': 'Delegation name',
                              'name': 'cn',
                              'primary_key': True,
                              'type': 'str'}],
 'servrole': [{'doc': 'IPA role name',
               'label': 'Role name',
               'name': 'name',
               'primary_key': True,
               'type': 'str'}],
 'stageuser': [{'label': 'User login',
                'name': 'uid',
                'primary_key': True,
                'type': 'str'}],
 'sudocmd': [{'label': 'Sudo Command',
              'name': 'sudocmd',
              'primary_key': True,
              'type': 'str'}],
 'sudocmdgroup': [{'label': 'Sudo Command Group',
                   'name': 'cn',
                   'primary_key': True,
                   'type': 'str'}],
 'sudorule': [{'label': 'Rule name',
               'name': 'cn',
               'primary_key': True,
               'type': 'str'}],
 'topic': [{'label': 'Full name',
            'name': 'full_name',
            'primary_key': True,
            'type': 'str'}],
 'topologysegment': [{'doc': 'Arbitrary string identifying the segment',
                      'label': 'Segment name',
                      'name': 'cn',
                      'primary_key': True,
                      'type': 'str'}],
 'topologysuffix': [{'label': 'Suffix name',
                     'name': 'cn',
                     'primary_key': True,
                     'type': 'str'}],
 'trust': [{'label': 'Realm name',
            'name': 'cn',
            'primary_key': True,
            'type': 'str'}],
 'trustconfig': [],
 'trustdomain': [{'label': 'Domain name',
                  'name': 'cn',
                  'primary_key': True,
                  'type': 'str'}],
 'user': [{'label': 'User login',
           'name': 'uid',
           'primary_key': True,
           'type': 'str'}],
 'userstatus': [],
 'vault': [{'label': 'Vault name',
            'name': 'cn',
            'primary_key': True,
            'type': 'str'}],
 'vaultconfig': [],
 'vaultcontainer': []}

Vault container uses complex way of representing own RDN, it is not based on a primary key. It mixes in three different types via cn=<service>,cn=services, cn=shared, and cn=<user>,cn=users within the vault container. But it is something that is not possible to rename.

Metadata