freeipa/ipalib/plugins/pwpolicy.py
Rob Crittenden dac224c25a Add support for per-group kerberos password policy.
Use a Class of Service template to do per-group password policy. The
design calls for non-overlapping groups but with cospriority we can
still make sense of things.

The password policy entries stored under the REALM are keyed only on
the group name because the MIT ldap plugin can't handle quotes in the
DN. It also can't handle spaces between elements in the DN.
2009-10-05 13:29:55 -06:00

349 lines
11 KiB
Python

# Authors:
# Rob Crittenden <rcritten@redhat.com>
# Pavel Zuna <pzuna@redhat.com>
#
# Copyright (C) 2008 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 only
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
Password policy
"""
from ipalib import api, crud, errors
from ipalib import Command, Object
from ipalib import Int, Str
from ldap.functions import explode_dn
_fields = {
'group': 'Group policy',
'krbminpwdlife': 'Minimum lifetime (in hours)',
'krbmaxpwdlife': 'Maximum lifetime (in days)',
'krbpwdmindiffchars': 'Minimum number of characters classes',
'krbpwdminlength': 'Minimum length',
'krbpwdhistorylength': 'History size',
}
def _convert_time_for_output(entry_attrs):
# Convert seconds to hours and days for displaying to user
if 'krbmaxpwdlife' in entry_attrs:
entry_attrs['krbmaxpwdlife'][0] = str(
int(entry_attrs['krbmaxpwdlife'][0]) / 86400
)
if 'krbminpwdlife' in entry_attrs:
entry_attrs['krbminpwdlife'][0] = str(
int(entry_attrs['krbminpwdlife'][0]) / 3600
)
def _convert_time_on_input(entry_attrs):
# Convert hours and days to seconds for writing to LDAP
if 'krbmaxpwdlife' in entry_attrs:
entry_attrs['krbmaxpwdlife'] = entry_attrs['krbmaxpwdlife'] * 86400
if 'krbminpwdlife' in entry_attrs:
entry_attrs['krbminpwdlife'] = entry_attrs['krbminpwdlife'] * 3600
def make_cos_entry(group, cospriority=None):
"""
Make the CoS dn and entry for this group.
Returns (cos_dn, cos_entry) where:
cos_dn = DN of the new CoS entry
cos_entry = entry representing this new object
"""
try:
(groupdn, group_attrs) = api.Command['group_show'](group)
except errors.NotFound:
raise errors.NotFound(reason="group '%s' does not exist" % group)
cos_entry = {}
if cospriority:
cos_entry['cospriority'] = cospriority
cos_entry['objectclass'] = ['top', 'costemplate', 'extensibleobject', 'krbcontainer']
cos_dn = 'cn=\"%s\", cn=cosTemplates, cn=accounts, %s' % (groupdn, api.env.basedn)
return (cos_dn, cos_entry)
def make_policy_entry(group_cn, policy_entry):
"""
Make the krbpwdpolicy dn and entry for this group.
Returns (policy_dn, policy_entry) where:
policy_dn = DN of the new password policy entry
policy_entry = entry representing this new object
"""
# This DN must *NOT* have spaces between elements
policy_dn = "cn=%s,cn=%s,cn=kerberos,%s" % (group_cn, api.env.realm, api.env.basedn)
# Create the krb password policy entry. This MUST be located
# in the same container as the REALM or the kldap plugin won't
# recognize it. The usual CoS trick of putting the whole DN into
# the dn won't work either because the kldap plugin doesn't like
# quotes in the DN.
policy_entry['objectclass'] = ['top', 'nscontainer', 'krbpwdpolicy']
policy_entry['cn'] = group_cn
return (policy_dn, policy_entry)
class pwpolicy(Object):
"""
Password Policy object.
"""
takes_params = (
Int('krbmaxpwdlife?',
cli_name='maxlife',
doc='Max. Password Lifetime (days)',
minvalue=0,
attribute=True,
),
Int('krbminpwdlife?',
cli_name='minlife',
doc='Min. Password Lifetime (hours)',
minvalue=0,
attribute=True,
),
Int('krbpwdhistorylength?',
cli_name='history',
doc='Password History Size',
minvalue=0,
attribute=True,
),
Int('krbpwdmindiffchars?',
cli_name='minclasses',
doc='Min. Number of Character Classes',
minvalue=0,
attribute=True,
),
Int('krbpwdminlength?',
cli_name='minlength',
doc='Min. Length of Password',
minvalue=0,
attribute=True,
),
)
api.register(pwpolicy)
class pwpolicy_add(crud.Create):
"""
Create a new password policy associated with a group.
"""
takes_options = (
Str('group',
doc='Group to set policy for',
attribute=False,
),
Int('cospriority',
cli_name='priority',
doc='Priority of the policy. Higher number equals higher priority',
minvalue=0,
attribute=True,
),
)
def execute(self, *args, **options):
ldap = self.api.Backend.ldap2
group_cn = options['group']
# Create the CoS template
(cos_dn, cos_entry) = make_cos_entry(group_cn, options.get('cospriority', None))
if 'cospriority' in options:
del options['cospriority']
# Create the new password policy
policy_entry = self.args_options_2_entry(*args, **options)
(policy_dn, policy_entry) = make_policy_entry(group_cn, policy_entry)
_convert_time_on_input(policy_entry)
# Link the two entries together
cos_entry['krbpwdpolicyreference'] = policy_dn
ldap.add_entry(policy_dn, policy_entry, normalize=False)
ldap.add_entry(cos_dn, cos_entry, normalize=False)
# The policy is what is interesting, return that
(dn, entry_attrs) = ldap.get_entry(policy_dn, policy_entry.keys())
_convert_time_for_output(entry_attrs)
return (dn, entry_attrs)
def output_for_cli(self, textui, result, *args, **options):
# textui.print_name(self.name)
# textui.print_dashed("Added policy for '%s'." % options['group'])
(dn, entry_attrs) = result
textui.print_name(self.name)
textui.print_plain('Password policy:')
for (k, v) in _fields.iteritems():
if k in entry_attrs:
textui.print_attribute(v, entry_attrs[k])
textui.print_dashed('Modified password policy.')
api.register(pwpolicy_add)
class pwpolicy_mod(crud.Update):
"""
Modify password policy.
"""
takes_options = (
Str('group?',
doc='Group to set policy for',
attribute=False,
),
Int('cospriority?',
cli_name='priority',
doc='Priority of the policy. Higher number equals higher priority',
minvalue=0,
attribute=True,
),
)
def execute(self, *args, **options):
assert 'dn' not in options
ldap = self.api.Backend.ldap2
if not 'group' in options:
dn = self.api.env.container_accounts
entry_attrs = self.args_options_2_entry(*args, **options)
else:
entry_attrs = self.args_options_2_entry(*args, **options)
(dn, entry_attrs) = make_policy_entry(options['group'], entry_attrs)
_convert_time_on_input(entry_attrs)
try:
ldap.update_entry(dn, entry_attrs)
except errors.EmptyModlist:
pass
(dn, entry_attrs) = ldap.get_entry(dn, entry_attrs.keys())
_convert_time_for_output(entry_attrs)
return (dn, entry_attrs)
def output_for_cli(self, textui, result, *args, **options):
(dn, entry_attrs) = result
textui.print_name(self.name)
textui.print_plain('Password policy:')
for (k, v) in _fields.iteritems():
if k in entry_attrs:
textui.print_attribute(v, entry_attrs[k])
textui.print_dashed('Modified password policy.')
api.register(pwpolicy_mod)
class pwpolicy_del(crud.Delete):
"""
Delete a group password policy.
"""
takes_options = (
Str('group',
doc='Group to remove policy from',
),
)
def execute(self, *args, **options):
assert 'dn' not in options
ldap = self.api.Backend.ldap2
group_cn = options['group']
# Get the DN of the CoS template to delete
try:
(cos_dn, cos_entry) = make_cos_entry(group_cn, None)
except errors.NotFound:
# Ok, perhaps the group was deleted, try to make the group DN
rdn = ldap.make_rdn_from_attr('cn', group_cn)
group_dn = ldap.make_dn_from_rdn(rdn, api.env.container_group)
cos_dn = 'cn=\"%s\", cn=cosTemplates, cn=accounts, %s' % (group_dn, api.env.basedn)
policy_entry = self.args_options_2_entry(*args, **options)
(policy_dn, policy_entry) = make_policy_entry(group_cn, policy_entry)
ldap.delete_entry(policy_dn, normalize=False)
ldap.delete_entry(cos_dn, normalize=False)
return True
def output_for_cli(self, textui, result, *args, **options):
textui.print_name(self.name)
textui.print_dashed('Deleted policy "%s".' % options['group'])
api.register(pwpolicy_del)
class pwpolicy_show(Command):
"""
Display password policy.
"""
takes_options = (
Str('group?',
doc='Group to display policy',
),
Str('user?',
doc='Display policy applied to a given user',
),
)
def execute(self, *args, **options):
ldap = self.api.Backend.ldap2
dn = None
group = None
if 'user' in options:
rdn = ldap.make_rdn_from_attr('uid', options['user'])
user_dn = ldap.make_dn_from_rdn(rdn, api.env.container_user)
try:
(user_dn, user_attrs) = ldap.get_entry(user_dn, ['krbpwdpolicyreference'])
if 'krbpwdpolicyreference' in user_attrs:
dn = user_attrs['krbpwdpolicyreference'][0]
rdn = explode_dn(dn)
group = rdn[0].replace('cn=','')
except errors.NotFound:
raise errors.NotFound(reason="user '%s' not found" % options['user'])
if dn is None:
if not 'group' in options:
dn = self.api.env.container_accounts
else:
policy_entry = self.args_options_2_entry(*args, **options)
(dn, policy_entry) = make_policy_entry(options['group'], policy_entry)
(dn, entry_attrs) = ldap.get_entry(dn)
if 'user' in options:
if group:
entry_attrs['group'] = group
else:
entry_attrs['group'] = 'global'
_convert_time_for_output(entry_attrs)
return (dn, entry_attrs)
def output_for_cli(self, textui, result, *args, **options):
(dn, entry_attrs) = result
textui.print_name(self.name)
textui.print_plain('Password policy:')
for (k, v) in _fields.iteritems():
if k in entry_attrs:
textui.print_attribute(v, entry_attrs[k])
api.register(pwpolicy_show)