On Thu, 2014-06-05 at 08:45 +0200, Jan Cholasta wrote:
> On 28.5.2014 22:44, Nathaniel McCallum wrote:
> > On Mon, 2014-05-26 at 16:57 +0200, Jan Cholasta wrote:
> >> On 13.5.2014 19:12, Nathaniel McCallum wrote:
> >>> On Tue, 2014-05-13 at 16:33 +0200, Jan Cholasta wrote:
> >>>> On 12.5.2014 21:02, Nathaniel McCallum wrote:
> >>>>> On Thu, 2014-05-08 at 13:51 -0400, Simo Sorce wrote:
> >>>>>> On Thu, 2014-05-08 at 12:26 -0400, Nathaniel McCallum wrote:
> >>>>>>> On Wed, 2014-05-07 at 11:17 -0400, Simo Sorce wrote:
> >>>>>>>> On Wed, 2014-05-07 at 09:54 -0400, Dmitri Pal wrote:
> >>>>>>>>> On 05/07/2014 09:05 AM, Nathaniel McCallum wrote:
> >>>>>>>>>> On Wed, 2014-05-07 at 11:42 +0200, Jan Cholasta wrote:
> >>>>>>>>>>> Hi,
> >>>>>>>>>>>
> >>>>>>>>>>> On 6.5.2014 17:08, Nathaniel McCallum wrote:
> >>>>>>>>>>>> On Tue, 2014-05-06 at 09:49 -0400, Nathaniel McCallum wrote:
> >>>>>>>>>>>>> On Mon, 2014-05-05 at 12:42 -0400, Nathaniel McCallum wrote:
> >>>>>>>>>>>>>> This also constitutes a rethinking of the token ACIs after the
> >>>>>>>>>>>>>> introduction of SELFDN support.
> >>>>>>>>>>>>>>
> >>>>>>>>>>>>>> Admins, as before, have full access to all token permissions.
> >>>>>>>>>>>>>>
> >>>>>>>>>>>>>> Normal users have read/search/compare access to all of the 
> >>>>>>>>>>>>>> non-secret
> >>>>>>>>>>>>>> data for tokens assigned to them, whether protected or 
> >>>>>>>>>>>>>> non-protected.
> >>>>>>>>>>>>>> Users can add or delete non-protected tokens and modify most 
> >>>>>>>>>>>>>> of their
> >>>>>>>>>>>>>> metadata. However they cannot create, delete or modify 
> >>>>>>>>>>>>>> protected tokens.
> >>>>>>>>>>>>>> Regardless of whether the token is protected or not, users 
> >>>>>>>>>>>>>> cannot change
> >>>>>>>>>>>>>> a token's ownership or unique identity.
> >>>>>>>>>>>>>>
> >>>>>>>>>>>>>> In contrast, admins can create protected tokens. This protects 
> >>>>>>>>>>>>>> the token
> >>>>>>>>>>>>>> from deletion or modification when assigned to users. 
> >>>>>>>>>>>>>> Additionally, when
> >>>>>>>>>>>>>> a user account is deleted, the assigned non-protected tokens 
> >>>>>>>>>>>>>> are deleted
> >>>>>>>>>>>>>> but the protected tokens are merely orphaned. This permits the 
> >>>>>>>>>>>>>> token to
> >>>>>>>>>>>>>> be reassigned without having to recreate it. This last point is
> >>>>>>>>>>>>>> particularly useful in the case of hardware tokens.
> >>>>>>>>>>>>>>
> >>>>>>>>>>>>>> https://fedorahosted.org/freeipa/ticket/4228
> >>>>>>>>>>>>>>
> >>>>>>>>>>>>>> NOTE: This patch depends on my patch 0048.
> >>>>>>>>>>>>> This new version makes ipatokenDisabled visible for token 
> >>>>>>>>>>>>> owners. It is
> >>>>>>>>>>>>> also writable if the token is non-protected. This additionally 
> >>>>>>>>>>>>> fixes:
> >>>>>>>>>>>>>
> >>>>>>>>>>>>> https://fedorahosted.org/freeipa/ticket/4259
> >>>>>>>>>>>> This new version changes the way the default value of protected 
> >>>>>>>>>>>> is setup
> >>>>>>>>>>>> in accordance with the changes made for the review of my patch 
> >>>>>>>>>>>> 0048.2.
> >>>>>>>>>>>>
> >>>>>>>>>>>> Nathaniel
> >>>>>>>>>>> Is using the ipatokenprotected attribute the final design?
> >>>>>>>>>> No. Alternate designs are welcome. The code is easy enough to 
> >>>>>>>>>> modify.
> >>>>>>>>>>
> >>>>>>>>>>> I did not dig too deep into this, but I think it might be better 
> >>>>>>>>>>> to
> >>>>>>>>>>> instead use the managedby attribute on a token to limit who can 
> >>>>>>>>>>> delete
> >>>>>>>>>>> (or administer in other way) the token. On otptoken-add, 
> >>>>>>>>>>> managedby would
> >>>>>>>>>>> be set to the "whoami" user DN, unless run with --protected, in 
> >>>>>>>>>>> which
> >>>>>>>>>>> case managedby would be left empty. Then, when deleting a user, 
> >>>>>>>>>>> the
> >>>>>>>>>>> token would be deleted only if the user manages the token.
> >>>>>>>>>> It seems to me that the mechanics of this are roughly the same as
> >>>>>>>>>> protected, just with a different syntax. The cost of this is more
> >>>>>>>>>> complex ACIs. In particular, we'd have to use two userdn clauses 
> >>>>>>>>>> (is
> >>>>>>>>>> this possible?) instead of a simple filter. If there is a clear 
> >>>>>>>>>> benefit,
> >>>>>>>>>> we can justify the more obtuse syntax.
> >>>>>>>>>
> >>>>>>>>> We usually try not to create new attributes until it is fully 
> >>>>>>>>> justified.
> >>>>>>>>> I would like Simo to chime in on this.
> >>>>>>>>
> >>>>>>>> I would also prefer to reuse existing attributes and mechanism if
> >>>>>>>> possible and if it will not preclude future work.
> >>>>>>>>
> >>>>>>>> In this case, it "sounds" like managed-by has the appropriate 
> >>>>>>>> meaning:
> >>>>>>>> "who manages the token ?" -> myself, admin, other, none ?
> >>>>>>>>
> >>>>>>>> Nathaniel can you send 2 lines showing the difference in ACIs between
> >>>>>>>> using managed-by vs a new attribute ?
> >>>>>>>
> >>>>>>> These are the ACIs using the protected mechanism:
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs =
> >>>>>>> "objectclass || description || ipatokenUniqueID || ipatokenDisabled ||
> >>>>>>> ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || 
> >>>>>>> ipatokenModel
> >>>>>>> || ipatokenSerial || ipatokenOwner || ipatokenProtected")(version 3.0;
> >>>>>>> acl "Users can read basic token info"; allow (read, search, compare)
> >>>>>>> userattr = "ipatokenOwner#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs =
> >>>>>>> "ipatokenOTPalgorithm || ipatokenOTPdigits ||
> >>>>>>> ipatokenTOTPtimeStep")(version 3.0; acl "Users can see TOTP details";
> >>>>>>> allow (read, search, compare) userattr = "ipatokenOwner#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs =
> >>>>>>> "ipatokenOTPalgorithm || ipatokenOTPdigits")(version 3.0; acl "Users 
> >>>>>>> can
> >>>>>>> see HOTP details"; allow (read, search, compare) userattr =
> >>>>>>> "ipatokenOwner#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter =
> >>>>>>> "(&(objectClass=ipaToken)(!(ipatokenProtected=TRUE)))")(targetattrs =
> >>>>>>> "description || ipatokenDisabled || ipatokenNotBefore ||
> >>>>>>> ipatokenNotAfter || ipatokenVendor || ipatokenModel ||
> >>>>>>> ipatokenSerial")(version 3.0; acl "Users can write basic token info";
> >>>>>>> allow (write) userattr = "ipatokenOwner#USERDN";)
> >>>>>>>
> >>>>>>> aci: (target = 
> >>>>>>> "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter
> >>>>>>> = "(&(objectClass=ipaToken)(!(ipatokenProtected=TRUE))))")(version 
> >>>>>>> 3.0;
> >>>>>>> acl "Users can create and delete tokens"; allow (add, delete) 
> >>>>>>> userattr =
> >>>>>>> "ipatokenOwner#SELFDN";)
> >>>>>>>
> >>>>>>> This is what they look like using managedBy (I have not tested this):
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs =
> >>>>>>> "objectclass || description || ipatokenUniqueID || ipatokenDisabled ||
> >>>>>>> ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || 
> >>>>>>> ipatokenModel
> >>>>>>> || ipatokenSerial || ipatokenOwner || ipatokenProtected")(version 3.0;
> >>>>>>> acl "Users can read basic token info"; allow (read, search, compare)
> >>>>>>> userattr = "ipatokenOwner#USERDN"; allow (read, search, compare)
> >>>>>>> userattr = "managedBy#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs =
> >>>>>>> "ipatokenOTPalgorithm || ipatokenOTPdigits ||
> >>>>>>> ipatokenTOTPtimeStep")(version 3.0; acl "Users can see TOTP details";
> >>>>>>> allow (read, search, compare) userattr = "ipatokenOwner#USERDN"; allow
> >>>>>>> (read, search, compare) userattr = "managedBy#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs =
> >>>>>>> "ipatokenOTPalgorithm || ipatokenOTPdigits")(version 3.0; acl "Users 
> >>>>>>> can
> >>>>>>> see HOTP details"; allow (read, search, compare) userattr =
> >>>>>>> "ipatokenOwner#USERDN"; allow (read, search, compare) userattr =
> >>>>>>> "managedBy#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs =
> >>>>>>> "description || ipatokenDisabled || ipatokenNotBefore ||
> >>>>>>> ipatokenNotAfter || ipatokenVendor || ipatokenModel ||
> >>>>>>> ipatokenSerial")(version 3.0; acl "Managers can write basic token 
> >>>>>>> info";
> >>>>>>> allow (write) userattr = "managedBy#USERDN";)
> >>>>>>>
> >>>>>>> aci: (targetfilter = "(objectClass=ipaToken)")(version 3.0; acl
> >>>>>>> "Managers can delete tokens"; allow (delete) userattr =
> >>>>>>> "managedBy#USERDN";)
> >>>>>>>
> >>>>>>> aci: (target = 
> >>>>>>> "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter
> >>>>>>> = "(objectClass=ipaToken)")(version 3.0; acl "Users can create
> >>>>>>> self-managed tokens"; allow (add) userattr = "ipatokenOwner#SELFDN" 
> >>>>>>> and
> >>>>>>> userattr = "managedBy#SELFDN";)
> >>>>>>>
> >>>>>>> In short:
> >>>>>>> 1. Owner and manager get read, search and compare.
> >>>>>>> 2. Manager gets write (to select attributes) and delete.
> >>>>>>> 3. Users can create self-managed tokens for themselves only.
> >>>>>>>
> >>>>>>> The otptoken-add command should gain the following defaults:
> >>>>>>> 1. The owner defaults to the user adding the token.
> >>>>>>> 2. If owner == user adding token, managedBy defaults to owner.
> >>>>>>> 3. Otherwise, managedBy defaults to None.
> >>>>>>>
> >>>>>>> This means that if neither owner nor managedBy are specified, the
> >>>>>>> default is a self-owned, self-managed token. Likewise, if a different
> >>>>>>> owner is specified, no manager is assumed.
> >>>>>>>
> >>>>>>> rcrit expresses worry that ipalib's ACI parser may not handle the 
> >>>>>>> above
> >>>>>>> syntax. This will become clear during testing if we want this 
> >>>>>>> approach.
> >>>>>>>
> >>>>>>> Does this look sane?
> >>>>>>
> >>>>>> I am not entirely sure your ACI syntax is always right for the second
> >>>>>> set. and perhaps we want to duplicate ACIs in some cases (once for 
> >>>>>> owner
> >>>>>> once for manager).
> >>>>>>
> >>>>>> I think the read ACIs do not need to list managedby ? Do we plan to 
> >>>>>> have
> >>>>>> a manager that is another regular user but not the owner nor an admin ?
> >>>>>>
> >>>>>> In any case I prefer the sytnax that uses managedby, as it has more
> >>>>>> potential.
> >>>>>
> >>>>> Attached is a new version of the patch which implements the feature
> >>>>> using managedBy instead of ipatokenProtected. One important thing needs
> >>>>> to be said about this patch. I am not exposing managedBy in either the
> >>>>> UI, the CLI or LDAP (ACI). Do we care about this? If yes, should I
> >>>>> expose this similar to owner or as a relationship?
> >>>>
> >>>> I would expose it, as a relationship. (Note that ipatokenowner should
> >>>> ideally be represented as a relationship too, but the framework does not
> >>>> support 1-to-many relationships ATM.)
> >>>
> >>> So since this is a 1-to-many relationship we shouldn't expose it?
> >>>
> >>> Or should I do it like owner is currently done?
> >>
> >> Do it like managedby is done in the host plugin (see
> >> "attribute_members", "relationships", etc.)
> >>
> >>>
> >>>>
> >>>> Just curious, why are the ACIs divided like this:
> >>>>
> >>>>        aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs =
> >>>> "objectclass || description || ipatokenUniqueID || ipatokenDisabled ||
> >>>> ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel
> >>>> || ipatokenSerial || ipatokenOwner")(version 3.0; acl "Users/managers
> >>>> can read basic token info"; allow (read, search, compare) userattr =
> >>>> "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)
> >>>>        aci: (targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs =
> >>>> "ipatokenOTPalgorithm || ipatokenOTPdigits ||
> >>>> ipatokenTOTPtimeStep")(version 3.0; acl "Users/managers can see TOTP
> >>>> details"; allow (read, search, compare) userattr =
> >>>> "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)
> >>>>        aci: (targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs =
> >>>> "ipatokenOTPalgorithm || ipatokenOTPdigits")(version 3.0; acl
> >>>> "Users/managers can see HOTP details"; allow (read, search, compare)
> >>>> userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)
> >>>>
> >>>> IMHO you could make them less complex by dividing them like this:
> >>>>
> >>>>        aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs =
> >>>> "objectclass || description || ipatokenUniqueID || ipatokenDisabled ||
> >>>> ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel
> >>>> || ipatokenSerial || ipatokenOwner || ipatokenOTPalgorithm ||
> >>>> ipatokenOTPdigits || ipatokenTOTPtimeStep")(version 3.0; acl "Owner can
> >>>> read token details"; allow (read, search, compare) userattr =
> >>>> "ipatokenOwner#USERDN";)
> >>>>        aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs =
> >>>> "objectclass || description || ipatokenUniqueID || ipatokenDisabled ||
> >>>> ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel
> >>>> || ipatokenSerial || ipatokenOwner || ipatokenOTPalgorithm ||
> >>>> ipatokenOTPdigits || ipatokenTOTPtimeStep")(version 3.0; acl "Managers
> >>>> can read token details"; allow (read, search, compare) userattr =
> >>>> "managedBy#USERDN";)
> >>>
> >>> The first set is organized by objectClass. The second by userattr. I
> >>> have no strong opinion on this matter, though performance is probably a
> >>> consideration. Do any DS guys want to chime in?
> >>
> >> I would still like to know someone else's opinion on this, but if
> >> there's none, let's keep it your way.
> >>
> >>>
> >>>> Would it make sense to keep --protected as a flag in otptoken-add as a
> >>>> shortcut for "entry_attrs['managedby'] = None"?
> >>>
> >>> I can't think of a use case for this. The only use case I *can* think of
> >>> is an admin creating a non-protected token for a user.
> >>
> >> OK.
> >>
> >>>
> >>>> Would it make sense to default managedby to the current bind DN in
> >>>> otptoken-add, even if it's not a user DN? (Do we want to allow running
> >>>> otptoken-add by hosts/services/other non-users?)
> >>>
> >>> No idea. Dmitri?
> >>
> >> We can add this later if necessary.
> >>
> >>>
> >>>> Is orphaning a token when a user is deleted only if it is not managed by
> >>>> any other users the intended behavior? It just seems sort of strange to
> >>>> me, since it changes the token from unprotected to protected.
> >>>
> >>> I don't think that is the behavior. We orphan the token if the owner is
> >>> not listed as a manager. If the owner is listed as a manager, we delete
> >>> the token.
> >>>
> >>> Put another way, protected tokens are orphaned and unprotected tokens
> >>> are deleted.
> >>>
> >>> If I didn't implement that, please point out my bug.
> >>
> >> Sorry, my bad, your code is right. You can make it simpler, though:
> >>
> >>       orphan = [x for x in token.get('managedby', []) if x == dn]
> >>
> >> (The "len() == 0" check is not necessary and using list comprehensions
> >> makes the code more readable than using filter.)
> >
> > The attached version fixes all the above issues. Two issues that may
> > remain:
> > 1. There is no option to set managedBy during otptoken-add or
> > otptoken-mod. This is probably okay.
> 
> Yes. I guess this bit is not needed anymore:
> 
>           # If owner was not specified, default to the person adding 
> this token.
> -        if 'ipatokenowner' not in entry_attrs:
> +        # If managedby was not specified, attempt a sensible default.
> +        if 'ipatokenowner' not in entry_attrs or 'managedby' not in 
> entry_attrs:
>               result = self.api.Command.user_find(whoami=True)['result']
>               if result:
>                   cur_uid = result[0]['uid'][0]
> -                entry_attrs.setdefault('ipatokenowner', cur_uid)
> +                prev_uid = entry_attrs.setdefault('ipatokenowner', cur_uid)
> +                if cur_uid == prev_uid:
> +                    entry_attrs.setdefault('managedby', result[0]['dn'])

This code is still needed. Read the patch description. And, FYI,
'managedby' is correct in this case, not 'managedby_user'. I tested.

> > 2. I can't figure out how to get the framework to actually show
> > managedBy in command output (like otptoken-show). This means you can
> > add/remove relationships using otptoken-add-managedby and
> > otptoken-remove-managedby, but you can't actually see the list of
> > managers. What am I missing?
> 
> In the hbacrule or selinuxusermap plugins it is done by adding an 
> "invisible" param to the object plugin, like this:
> 
>      Str('managedby_user?',
>          label=_('Manager'),
>          flags=['no_create', 'no_update', 'no_search'],
>      ),

Done.

> >
> > Also, it would be helpful if someone with DS expertise could answer the
> > question about the performance of the ACI structure options as listed
> > above.

We still need this. Ludwig?

> You should update the code in user_del to use managedby_user instead of 
> managedby, otherwise it won't really work.

Done.
>From 52faab66405918caa3753e84970b88fa0fb98862 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Fri, 2 May 2014 16:44:30 -0400
Subject: [PATCH] Add support for managedBy to tokens

This also constitutes a rethinking of the token ACIs after the introduction
of SELFDN support.

Admins, as before, have full access to all token permissions.

Normal users have read/search/compare access to all of the non-secret data
for tokens assigned to them, whether managed by them or not. Users can add
tokens if, and only if, they will also manage this token.

Managers can also read/search/compare tokens they manage. Additionally,
they can write non-secret data to their managed tokens and delete them.

When a normal user self-creates a token (the default behavior), then
managedBy is automatically set. When an admin creates a token for another
user (or no owner is assigned at all), then managed by is not set. In this
second case, the token is effectively read-only for the assigned owner.

This behavior enables two important other behaviors. First, an admin can
create a hardware token and assign it to the user as a read-only token.
Second, when the user is deleted, only his self-managed tokens are deleted.
All other (read-only) tokens are instead orphaned. This permits the same
token object to be reasigned to another user without loss of any counter
data.

https://fedorahosted.org/freeipa/ticket/4228
https://fedorahosted.org/freeipa/ticket/4259
---
 API.txt                        | 34 ++++++++++++++++++++++++++++++----
 VERSION                        |  4 ++--
 install/share/70ipaotp.ldif    |  2 +-
 install/share/default-aci.ldif | 11 ++++++-----
 install/updates/40-otp.update  | 16 +++++++++++-----
 ipalib/plugins/otptoken.py     | 38 +++++++++++++++++++++++++++++++-------
 ipalib/plugins/user.py         |  9 +++++++--
 7 files changed, 88 insertions(+), 26 deletions(-)

diff --git a/API.txt b/API.txt
index 84769522bcd1cdbb4fc3748a950fbb3c6771e7ad..30974d92ef375d5132cecb294700742d99086143 100644
--- a/API.txt
+++ b/API.txt
@@ -2226,7 +2226,7 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: otptoken_add
-args: 1,21,3
+args: 1,22,3
 arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, required=False)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -2244,6 +2244,7 @@ option: Str('ipatokenserial', attribute=True, autofill=True, cli_name='serial',
 option: Int('ipatokentotpclockoffset', attribute=True, autofill=True, cli_name='offset', default=0, multivalue=False, required=False)
 option: Int('ipatokentotptimestep', attribute=True, autofill=True, cli_name='interval', default=30, minvalue=5, multivalue=False, required=False)
 option: Str('ipatokenvendor', attribute=True, autofill=True, cli_name='vendor', default=u'FreeIPA', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('qrcode?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('setattr*', cli_name='setattr', exclude='webui')
@@ -2252,6 +2253,17 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: otptoken_add_managedby
+args: 1,5,3
+arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: otptoken_del
 args: 1,2,3
 arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=True, primary_key=True, query=True, required=True)
@@ -2261,7 +2273,7 @@ output: Output('result', <type 'dict'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: ListOfPrimaryKeys('value', None, None)
 command: otptoken_find
-args: 1,21,4
+args: 1,22,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
@@ -2278,6 +2290,7 @@ option: Int('ipatokentotpclockoffset', attribute=True, autofill=False, cli_name=
 option: Int('ipatokentotptimestep', attribute=True, autofill=False, cli_name='interval', default=30, minvalue=5, multivalue=False, query=True, required=False)
 option: Str('ipatokenuniqueid', attribute=True, autofill=False, cli_name='id', multivalue=False, primary_key=True, query=True, required=False)
 option: Str('ipatokenvendor', attribute=True, autofill=False, cli_name='vendor', default=u'FreeIPA', multivalue=False, query=True, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('pkey_only?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Int('sizelimit?', autofill=False, minvalue=0)
@@ -2289,7 +2302,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: otptoken_mod
-args: 1,16,3
+args: 1,17,3
 arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -2302,6 +2315,7 @@ option: Str('ipatokennotbefore', attribute=True, autofill=False, cli_name='not_b
 option: Str('ipatokenowner', attribute=True, autofill=False, cli_name='owner', multivalue=False, required=False)
 option: Str('ipatokenserial', attribute=True, autofill=False, cli_name='serial', multivalue=False, required=False)
 option: Str('ipatokenvendor', attribute=True, autofill=False, cli_name='vendor', default=u'FreeIPA', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('rename', cli_name='rename', multivalue=False, primary_key=True, required=False)
 option: Flag('rights', autofill=True, default=False)
@@ -2310,10 +2324,22 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: otptoken_remove_managedby
+args: 1,5,3
+arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 command: otptoken_show
-args: 1,4,3
+args: 1,5,3
 arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
 option: Str('version?', exclude='webui')
diff --git a/VERSION b/VERSION
index 97b8f6fae0c23961c9d2b9330cc3dbad40ff992f..318b88a150638d6e55419527e18d5a283faa735a 100644
--- a/VERSION
+++ b/VERSION
@@ -89,5 +89,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=88
-# Last change: mbasti - Added 'dns_name_values' capability
+IPA_API_VERSION_MINOR=89
+# Last change: npmccallum - Add support for managedBy to tokens
diff --git a/install/share/70ipaotp.ldif b/install/share/70ipaotp.ldif
index a40ad9ee0cfcf72ed6b79306396a29683f9e1a9d..0b9815704cccabfa515c3a744de9ffa330f65500 100644
--- a/install/share/70ipaotp.ldif
+++ b/install/share/70ipaotp.ldif
@@ -23,7 +23,7 @@ attributeTypes: (2.16.840.1.113730.3.8.16.1.18 NAME 'ipatokenRadiusTimeout' DESC
 attributeTypes: (2.16.840.1.113730.3.8.16.1.19 NAME 'ipatokenRadiusRetries' DESC 'Number of allowed Retries' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA OTP')
 attributeTypes: (2.16.840.1.113730.3.8.16.1.20 NAME 'ipatokenUserMapAttribute' DESC 'Attribute to map from the user entry for RADIUS server authentication' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA OTP')
 attributeTypes: (2.16.840.1.113730.3.8.16.1.21 NAME 'ipatokenHOTPcounter' DESC 'HOTP counter' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA OTP')
-objectClasses:  (2.16.840.1.113730.3.8.16.2.1  NAME 'ipaToken' SUP top ABSTRACT DESC 'Abstract token class for tokens' MUST (ipatokenUniqueID) MAY (description $ ipatokenOwner $ ipatokenDisabled $ ipatokenNotBefore $ ipatokenNotAfter $ ipatokenVendor $ ipatokenModel $ ipatokenSerial) X-ORIGIN 'IPA OTP')
+objectClasses:  (2.16.840.1.113730.3.8.16.2.1  NAME 'ipaToken' SUP top ABSTRACT DESC 'Abstract token class for tokens' MUST (ipatokenUniqueID) MAY (description $ managedBy $ ipatokenOwner $ ipatokenDisabled $ ipatokenNotBefore $ ipatokenNotAfter $ ipatokenVendor $ ipatokenModel $ ipatokenSerial) X-ORIGIN 'IPA OTP')
 objectClasses:  (2.16.840.1.113730.3.8.16.2.2  NAME 'ipatokenTOTP' SUP ipaToken STRUCTURAL DESC 'TOTP Token Type' MUST (ipatokenOTPkey $ ipatokenOTPalgorithm $ ipatokenOTPdigits $ ipatokenTOTPclockOffset $ ipatokenTOTPtimeStep) X-ORIGIN 'IPA OTP')
 objectClasses:  (2.16.840.1.113730.3.8.16.2.3  NAME 'ipatokenRadiusProxyUser' SUP top AUXILIARY DESC 'Radius Proxy User' MAY (ipatokenRadiusConfigLink $ ipatokenRadiusUserName) X-ORIGIN 'IPA OTP')
 objectClasses:  (2.16.840.1.113730.3.8.16.2.4  NAME 'ipatokenRadiusConfiguration' SUP top STRUCTURAL DESC 'Proxy Radius Configuration' MUST (cn $ ipatokenRadiusServer $ ipatokenRadiusSecret) MAY (description $ ipatokenRadiusTimeout $ ipatokenRadiusRetries $ ipatokenUserMapAttribute) X-ORIGIN 'IPA OTP')
diff --git a/install/share/default-aci.ldif b/install/share/default-aci.ldif
index 04fc185f785ee71246c6cc4f958c754158f16302..a4a5d99544548daaaf88e1e393ed69b808c6715e 100644
--- a/install/share/default-aci.ldif
+++ b/install/share/default-aci.ldif
@@ -84,8 +84,9 @@ aci: (target="ldap:///cn=*,cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX";)(targetattr="use
 dn: $SUFFIX
 changetype: modify
 add: aci
-aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can read basic token info"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN";)
-aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs = "ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can write basic token info"; allow (write) userattr = "ipatokenOwner#USERDN";)
-aci: (targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPclockOffset || ipatokenTOTPtimeStep")(version 3.0; acl "Users can add TOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)
-aci: (target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create and delete tokens"; allow (add, delete) userattr = "ipatokenOwner#SELFDN";)
-aci: (targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenHOTPcounter")(version 3.0; acl "Users can add HOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)
+aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || description || managedBy || ipatokenUniqueID || ipatokenDisabled || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial || ipatokenOwner")(version 3.0; acl "Users/managers can read basic token info"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)
+aci: (targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPtimeStep")(version 3.0; acl "Users/managers can see TOTP details"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)
+aci: (targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPalgorithm || ipatokenOTPdigits")(version 3.0; acl "Users/managers can see HOTP details"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)
+aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs = "description || ipatokenDisabled || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Managers can write basic token info"; allow (write) userattr = "managedBy#USERDN";)
+aci: (targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Managers can delete tokens"; allow (delete) userattr = "managedBy#USERDN";)
+aci: (target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create self-managed tokens"; allow (add) userattr = "ipatokenOwner#SELFDN" and userattr = "managedBy#SELFDN";)
diff --git a/install/updates/40-otp.update b/install/updates/40-otp.update
index ba9be5f0569edffea6fae6aaee031012414f9353..caa21c380618dd6450205b0883c6165e9ac40967 100644
--- a/install/updates/40-otp.update
+++ b/install/updates/40-otp.update
@@ -4,11 +4,17 @@ default: objectClass: top
 default: cn: otp
 
 dn: $SUFFIX
-add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can read basic token info"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN";)'
-add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can write basic token info"; allow (write) userattr = "ipatokenOwner#USERDN";)'
-add: aci:'(targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPclockOffset || ipatokenTOTPtimeStep")(version 3.0; acl "Users can add TOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
-add: aci:'(target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create and delete tokens"; allow (add, delete) userattr = "ipatokenOwner#SELFDN";)'
-add: aci:'(targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenHOTPcounter")(version 3.0; acl "Users can add HOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
+remove: aci:'(target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create and delete tokens"; allow (add, delete) userattr = "ipatokenOwner#SELFDN";)'
+remove: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can read basic token info"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN";)'
+remove: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can write basic token info"; allow (write) userattr = "ipatokenOwner#USERDN";)'
+remove: aci:'(targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPclockOffset || ipatokenTOTPtimeStep")(version 3.0; acl "Users can add TOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
+remove: aci:'(targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenHOTPcounter")(version 3.0; acl "Users can add HOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
+add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || description || managedBy || ipatokenUniqueID || ipatokenDisabled || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial || ipatokenOwner")(version 3.0; acl "Users/managers can read basic token info"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)'
+add: aci:'(targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPtimeStep")(version 3.0; acl "Users/managers can see TOTP details"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)'
+add: aci:'(targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPalgorithm || ipatokenOTPdigits")(version 3.0; acl "Users/managers can see HOTP details"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN" or userattr = "managedBy#USERDN";)'
+add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "description || ipatokenDisabled || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Managers can write basic token info"; allow (write) userattr = "managedBy#USERDN";)'
+add: aci:'(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Managers can delete tokens"; allow (delete) userattr = "managedBy#USERDN";)'
+add: aci:'(target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX";)(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create self-managed tokens"; allow (add) userattr = "ipatokenOwner#SELFDN" and userattr = "managedBy#SELFDN";)'
 
 dn: cn=radiusproxy,$SUFFIX
 default: objectClass: nsContainer
diff --git a/ipalib/plugins/otptoken.py b/ipalib/plugins/otptoken.py
index b264287c322381fb99c8823f7b1505ec537973ad..72b22fba933593929ae63da3b48c30d005de9a5b 100644
--- a/ipalib/plugins/otptoken.py
+++ b/ipalib/plugins/otptoken.py
@@ -17,7 +17,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from ipalib.plugins.baseldap import DN, LDAPObject, LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve
+from ipalib.plugins.baseldap import DN, LDAPObject, LDAPAddMember, LDAPRemoveMember
+from ipalib.plugins.baseldap import LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve
 from ipalib import api, Int, Str, Bool, Flag, Bytes, IntEnum, StrEnum, _, ngettext
 from ipalib.plugable import Registry
 from ipalib.errors import PasswordMismatch, ConversionError, LastMemberError, NotFound
@@ -109,8 +110,14 @@ class otptoken(LDAPObject):
     default_attributes = [
         'ipatokenuniqueid', 'description', 'ipatokenowner',
         'ipatokendisabled', 'ipatokennotbefore', 'ipatokennotafter',
-        'ipatokenvendor', 'ipatokenmodel', 'ipatokenserial'
+        'ipatokenvendor', 'ipatokenmodel', 'ipatokenserial', 'managedby'
     ]
+    attribute_members = {
+        'managedby': ['user'],
+    }
+    relationships = {
+        'managedby': ('Managed by', 'man_by_', 'not_man_by_'),
+    }
     rdn_is_primary_key = True
 
     label = _('OTP Tokens')
@@ -138,6 +145,10 @@ class otptoken(LDAPObject):
             cli_name='owner',
             label=_('Owner'),
         ),
+        Str('managedby_user?',
+            label=_('Manager'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
         Bool('ipatokendisabled?',
             cli_name='disabled',
             label=_('Disabled state')
@@ -245,11 +256,14 @@ class otptoken_add(LDAPCreate):
                         del entry_attrs[tattr]
 
         # If owner was not specified, default to the person adding this token.
-        if 'ipatokenowner' not in entry_attrs:
+        # If managedby was not specified, attempt a sensible default.
+        if 'ipatokenowner' not in entry_attrs or 'managedby' not in entry_attrs:
             result = self.api.Command.user_find(whoami=True)['result']
             if result:
                 cur_uid = result[0]['uid'][0]
-                entry_attrs.setdefault('ipatokenowner', cur_uid)
+                prev_uid = entry_attrs.setdefault('ipatokenowner', cur_uid)
+                if cur_uid == prev_uid:
+                    entry_attrs.setdefault('managedby', result[0]['dn'])
 
         # Resolve the owner's dn
         _normalize_owner(self.api.Object.user, entry_attrs)
@@ -326,9 +340,7 @@ class otptoken_mod(LDAPUpdate):
 @register()
 class otptoken_find(LDAPSearch):
     __doc__ = _('Search for OTP token.')
-    msg_summary = ngettext(
-        '%(count)d OTP token matched', '%(count)d OTP tokens matched', 0
-    )
+    msg_summary = ngettext('%(count)d OTP token matched', '%(count)d OTP tokens matched', 0)
 
     def pre_callback(self, ldap, filters, *args, **kwargs):
         # This is a hack, but there is no other way to
@@ -359,3 +371,15 @@ class otptoken_show(LDAPRetrieve):
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         _convert_owner(self.api.Object.user, entry_attrs, options)
         return super(otptoken_show, self).post_callback(ldap, dn, entry_attrs, *keys, **options)
+
+@register()
+class otptoken_add_managedby(LDAPAddMember):
+    __doc__ = _('Add users that can manage this token.')
+
+    member_attributes = ['managedby']
+
+@register()
+class otptoken_remove_managedby(LDAPRemoveMember):
+    __doc__ = _('Remove hosts that can manage this host.')
+
+    member_attributes = ['managedby']
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 27ad36b7fdbee5b2c0dba10f1c381233e8059150..da8c5214fcf3e205e222386f10699e8c84c390cb 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -771,12 +771,17 @@ class user_del(LDAPDelete):
         assert isinstance(dn, DN)
         check_protected_member(keys[-1])
 
-        # Delete all tokens owned by this user
+        # Delete all tokens owned and managed by this user.
+        # Orphan all tokens owned but not managed by this user.
         owner = self.api.Object.user.get_primary_key_from_dn(dn)
         results = self.api.Command.otptoken_find(ipatokenowner=owner)['result']
         for token in results:
+            orphan = not [x for x in token.get('managedby_user', []) if x == owner]
             token = self.api.Object.otptoken.get_primary_key_from_dn(token['dn'])
-            self.api.Command.otptoken_del(token)
+            if orphan:
+                self.api.Command.otptoken_mod(token, ipatokenowner=None)
+            else:
+                self.api.Command.otptoken_del(token)
 
         return dn
 
-- 
2.0.0

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to