On Срд, 25 кас 2023, Ales Rozmarin via FreeIPA-users wrote:
Hi Alexander,
I create objectClasses named 'testHost' which have attribute 'host'.
As I mention up, I also test to create new attribute like this
attributeTypes: ( 2.25.36.1.2.3.4.5.1 NAME 'customhost' DESC 'A hostname or identifier for a Custom host' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Extending FreeIPA') # ############################################################################### # objectClasses: ( 2.25.36.1.2.3.4.5.2 NAME 'testHost' DESC 'An object class for Custom hosts' SUP person STRUCTURAL MAY (customhost) X-ORIGIN 'Extending FreeIPA' )
and not use default one:
attributeTypes: ( 0.9.2342.19200300.100.1.9 NAME 'host' EQUALITY caseIgnoreMa tch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-O RIGIN 'RFC 4524' )
I get only error when I use default attribute host it says, invalid 'ipauserobjectclasses': user default attribute host would not be allowed!
And that is correct because you don't have an object class that would allow having that attribute in the list of default ones.
That why I'm not sure if I'm doing something wrong or is some restriction that we can't display attribute 'host' in User Web UI.
You have two separate tasks here. First, if attribute exists in the entry, it needs to be displayed. To display it, it needs to be fetched from the LDAP entry, so the attribute needs to be specified in a list of attributes to be fetched. Second, for storing the attribute value if it doesn't exist, it needs to be added. For the latter you'd need an objectclass in the entry to allow attribute to be saved.
Attributes may be present at creation time or added afterwards, based on some conditions. Retrieval can be done differently depending on a logic. For a basic scenario where you'd always want to retrieve an attribute value if that one exists, you'd add the attribute name to the object'sa 'default_attributes' list.
'user' class is derived from the 'baseuser' -- there are two user classes in IPA, one used for normal users and one used for staged users, so we have common definitions in 'baseuser' and then inherit those by the actual objects:
class baseuser(LDAPObject): """ baseuser object. """ .... object_class_config = 'ipauserobjectclasses' possible_objectclasses = [ 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', 'ipatokenradiusproxyuser', 'ipacertmapobject', 'ipantuserattrs', 'ipaidpuser', 'ipapasskeyuser', ] .... default_attributes = [ 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', 'uidnumber', 'gidnumber', 'mail', 'ou', 'telephonenumber', 'title', 'memberof', 'nsaccountlock', 'memberofindirect', 'ipauserauthtype', 'userclass', 'ipatokenradiusconfiglink', 'ipatokenradiususername', 'ipaidpconfiglink', 'ipaidpsub', 'krbprincipalexpiration', 'usercertificate;binary', 'krbprincipalname', 'krbcanonicalname', 'ipacertmapdata', 'ipantlogonscript', 'ipantprofilepath', 'ipanthomedirectory', 'ipanthomedirectorydrive', 'ipapasskey', ]
The error you've got about an attribute would not be allowed comes from 'ipa config-mod' where you tried to modify list of attributes that would be added to every single user object upon creation. That code validates presence of the attribute against user class' possible_objectclasses:
for (attr, obj) in (('ipauserobjectclasses', 'user'), ('ipagroupobjectclasses', 'group')): if attr in entry_attrs: if not entry_attrs[attr]: raise errors.ValidationError(name=attr, error=_('May not be empty')) objectclasses = list(set(entry_attrs[attr]).union( self.api.Object[obj].possible_objectclasses)) new_allowed_attrs = ldap.get_allowed_attributes(objectclasses, raise_on_unknown=True) checked_attrs = self.api.Object[obj].default_attributes
.... for obj_attr in checked_attrs: .... if obj_attr.lower() not in new_allowed_attrs: raise errors.ValidationError(name=attr, error=_('%(obj)s default attribute %(attr)s would not be allowed!') \ % dict(obj=obj, attr=obj_attr))
So in your case a plugin would need to declare both the attribute and the objectclass in those lists:
------------------------ from ipaserver.user import user
user.default_attributes += ['customhost'] user.possible_objectclasses += ['testhost'] ------------------------
Adding attributes and object classes to the lists maintained by 'ipa config-mod' will only work for cases where you always have a value assigned to those attributes. 'ipa config-mod' maintains those lists for creating user and group objects, e.g. which object classes must be present in every user and group created by IPA API.
Your client-side code would then need to always supply one or have a default value that would be used by the parameter. This is not always desirable.
For example, your parameter definition does not have a default value (I corrected the attribute name):
user.takes_params += ( Str('customhost*', cli_name='host', label=_('Test host'), ), )
It means that you would not be able to use 'customhost' as a default one through 'ipa config-mod' because nothing would put a default value to this attribute in both Web UI (unless your plugin always adds one) and in IPA API (unless you have a client-side plugin that does so).
Instead of adding the attribute to the 'ipa config-mod', you should consider whether this attribute is indeed should always be present. If it is, then you should think about a default value. If there should be no default value, then don't add it to the default list.
When an attribute is sent by the client side (be it web UI or just some IPA API caller), the server side should also add a corresponding object class in a pre-callback. This is done automatically for object.possible_objectclasses list.
You don't see that in https://github.com/abbra/freeipa-userstatus-plugin/blob/master/plugin/ipaser... from where you started with your sample plugin because those attributes are already allowed by existing IPA object classes in the user entry.
For a conditional add of the objectclasses we have baseldap.add_missing_object_class method. For example, for users we do add objectclasses depending on what additional attributes were set in the entry. This is done with a callback that is commonly called by the internal code. External plugins can define their own pre-callback and do similar actions:
class baseuser_add(LDAPCreate): """ Prototype command plugin to be implemented by real plugin """ def pre_common_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): assert isinstance(dn, DN) set_krbcanonicalname(entry_attrs) self.obj.convert_usercertificate_pre(entry_attrs) if entry_attrs.get('ipatokenradiususername', None): add_missing_object_class(ldap, u'ipatokenradiusproxyuser', dn, entry_attrs, update=False) if entry_attrs.get('ipauserauthtype', None): add_missing_object_class(ldap, u'ipauserauthtypeclass', dn, entry_attrs, update=False) if ( entry_attrs.get('ipaidpconfiglink', None) or entry_attrs.get('ipaidpsub', None) ): add_missing_object_class(ldap, 'ipaidpuser', dn, entry_attrs, update=False)
I'd recommend looking at existing plugins when implementing a new one.