Merging with upstream

This commit is contained in:
Simo Sorce 2007-09-07 14:26:26 -04:00
commit 873bbbd2de
28 changed files with 1883 additions and 194 deletions

View File

@ -7,6 +7,10 @@ install:
install -m 755 ipa-finduser $(SBINDIR)
install -m 755 ipa-usermod $(SBINDIR)
install -m 755 ipa-deluser $(SBINDIR)
install -m 755 ipa-addgroup $(SBINDIR)
install -m 755 ipa-delgroup $(SBINDIR)
install -m 755 ipa-findgroup $(SBINDIR)
install -m 755 ipa-groupmod $(SBINDIR)
clean:
rm -f *~ *.pyc

View File

@ -50,7 +50,9 @@ def main():
client = ipaclient.IPAClient()
users = client.find_users(args[1], sattrs=['dn','uid','cn','homeDirectory'])
if len(users) == 0:
counter = users[0]
users = users[1:]
if counter == 0:
print "No entries found for", args[1]
return 0

View File

@ -59,6 +59,9 @@ def main():
except ipa.ipaerror.IPAError, e:
print "%s" % e.message
return 1
except kerberos.GSSError, e:
print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
return 1
if options.gecos:
user.setValue('gecos', options.gecos)
@ -79,6 +82,9 @@ def main():
except xmlrpclib.ProtocolError, e:
print "Unable to connect to IPA server: %s" % (e.errmsg)
return 1
except ipa.ipaerror.IPAError, e:
print "%s" % (e.message)
return 1
return 0

View File

@ -30,6 +30,7 @@ import stat
from string import lower
import re
import xmlrpclib
import datetime
def realm_to_suffix(realm_name):
s = realm_name.split(".")
@ -233,3 +234,100 @@ def unwrap_binary_data(data):
else:
return data
class GeneralizedTimeZone(datetime.tzinfo):
"""This class is a basic timezone wrapper for the offset specified
in a Generalized Time. It is dst-ignorant."""
def __init__(self,offsetstr="Z"):
super(GeneralizedTimeZone, self).__init__()
self.name = offsetstr
self.houroffset = 0
self.minoffset = 0
if offsetstr == "Z":
self.houroffset = 0
self.minoffset = 0
else:
if (len(offsetstr) >= 3) and re.match(r'[-+]\d\d', offsetstr):
self.houroffset = int(offsetstr[0:3])
offsetstr = offsetstr[3:]
if (len(offsetstr) >= 2) and re.match(r'\d\d', offsetstr):
self.minoffset = int(offsetstr[0:2])
offsetstr = offsetstr[2:]
if len(offsetstr) > 0:
raise ValueError()
if self.houroffset < 0:
self.minoffset *= -1
def utcoffset(self, dt):
return datetime.timedelta(hours=self.houroffset, minutes=self.minoffset)
def dst(self, dt):
return datetime.timedelta(0)
def tzname(self, dt):
return self.name
def parse_generalized_time(timestr):
"""Parses are Generalized Time string (as specified in X.680),
returning a datetime object. Generalized Times are stored inside
the krbPasswordExpiration attribute in LDAP.
This method doesn't attempt to be perfect wrt timezones. If python
can't be bothered to implement them, how can we..."""
if len(timestr) < 8:
return None
try:
date = timestr[:8]
time = timestr[8:]
year = int(date[:4])
month = int(date[4:6])
day = int(date[6:8])
hour = min = sec = msec = 0
tzone = None
if (len(time) >= 2) and re.match(r'\d', time[0]):
hour = int(time[:2])
time = time[2:]
if len(time) >= 2 and (time[0] == "," or time[0] == "."):
hour_fraction = "."
time = time[1:]
while (len(time) > 0) and re.match(r'\d', time[0]):
hour_fraction += time[0]
time = time[1:]
total_secs = int(float(hour_fraction) * 3600)
min, sec = divmod(total_secs, 60)
if (len(time) >= 2) and re.match(r'\d', time[0]):
min = int(time[:2])
time = time[2:]
if len(time) >= 2 and (time[0] == "," or time[0] == "."):
min_fraction = "."
time = time[1:]
while (len(time) > 0) and re.match(r'\d', time[0]):
min_fraction += time[0]
time = time[1:]
sec = int(float(min_fraction) * 60)
if (len(time) >= 2) and re.match(r'\d', time[0]):
sec = int(time[:2])
time = time[2:]
if len(time) >= 2 and (time[0] == "," or time[0] == "."):
sec_fraction = "."
time = time[1:]
while (len(time) > 0) and re.match(r'\d', time[0]):
sec_fraction += time[0]
time = time[1:]
msec = int(float(sec_fraction) * 1000000)
if (len(time) > 0):
tzone = GeneralizedTimeZone(time)
return datetime.datetime(year, month, day, hour, min, sec, msec, tzone)
except ValueError:
return None

View File

@ -21,6 +21,7 @@ import sys
sys.path.insert(0, ".")
import unittest
import datetime
import ipautil
@ -207,6 +208,102 @@ class TestCIDict(unittest.TestCase):
self.assert_(item in items)
items.discard(item)
class TestTimeParser(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def testSimple(self):
timestr = "20070803"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(2007, time.year)
self.assertEqual(8, time.month)
self.assertEqual(3, time.day)
self.assertEqual(0, time.hour)
self.assertEqual(0, time.minute)
self.assertEqual(0, time.second)
def testHourMinSec(self):
timestr = "20051213141205"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(2005, time.year)
self.assertEqual(12, time.month)
self.assertEqual(13, time.day)
self.assertEqual(14, time.hour)
self.assertEqual(12, time.minute)
self.assertEqual(5, time.second)
def testFractions(self):
timestr = "2003092208.5"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(2003, time.year)
self.assertEqual(9, time.month)
self.assertEqual(22, time.day)
self.assertEqual(8, time.hour)
self.assertEqual(30, time.minute)
self.assertEqual(0, time.second)
timestr = "199203301544,25"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(1992, time.year)
self.assertEqual(3, time.month)
self.assertEqual(30, time.day)
self.assertEqual(15, time.hour)
self.assertEqual(44, time.minute)
self.assertEqual(15, time.second)
timestr = "20060401185912,8"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(2006, time.year)
self.assertEqual(4, time.month)
self.assertEqual(1, time.day)
self.assertEqual(18, time.hour)
self.assertEqual(59, time.minute)
self.assertEqual(12, time.second)
self.assertEqual(800000, time.microsecond)
def testTimeZones(self):
timestr = "20051213141205Z"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(0, time.tzinfo.houroffset)
self.assertEqual(0, time.tzinfo.minoffset)
offset = time.tzinfo.utcoffset(None)
self.assertEqual(0, offset.seconds)
timestr = "20051213141205+0500"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(5, time.tzinfo.houroffset)
self.assertEqual(0, time.tzinfo.minoffset)
offset = time.tzinfo.utcoffset(None)
self.assertEqual(5 * 60 * 60, offset.seconds)
timestr = "20051213141205-0500"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(-5, time.tzinfo.houroffset)
self.assertEqual(0, time.tzinfo.minoffset)
# NOTE - the offset is always positive - it's minutes
# _east_ of UTC
offset = time.tzinfo.utcoffset(None)
self.assertEqual((24 - 5) * 60 * 60, offset.seconds)
timestr = "20051213141205-0930"
time = ipautil.parse_generalized_time(timestr)
self.assertEqual(-9, time.tzinfo.houroffset)
self.assertEqual(-30, time.tzinfo.minoffset)
offset = time.tzinfo.utcoffset(None)
self.assertEqual(((24 - 9) * 60 * 60) - (30 * 60), offset.seconds)
if __name__ == '__main__':
unittest.main()

View File

@ -50,6 +50,7 @@ rm -rf %{buildroot}
%attr(755,root,root) %{plugin_dir}/libipa_pwd_extop.so
%attr(755,root,root) %{plugin_dir}/libipa-memberof-plugin.so
%attr(755,root,root) %{plugin_dir}/libipa-dna-plugin.so
%changelog

View File

@ -50,6 +50,7 @@ rm -rf %{buildroot}
%attr(755,root,root) %{plugin_dir}/libipa_pwd_extop.so
%attr(755,root,root) %{plugin_dir}/libipa-memberof-plugin.so
%attr(755,root,root) %{plugin_dir}/libipa-dna-plugin.so
%changelog

View File

@ -17,6 +17,7 @@ import ipa.ipaclient
import ipa.user
import xmlrpclib
import forms.user
from helpers import userhelper
from ipa import ipaerror
ipa.config.init_config()
@ -47,6 +48,14 @@ class Root(controllers.RootController):
def index(self):
return dict()
@expose()
def topsearch(self, **kw):
if kw.get('searchtype') == "Users":
return self.userlist(uid=kw.get('searchvalue'))
else:
return self.index()
########
# User #
@ -107,7 +116,7 @@ class Root(controllers.RootController):
def userupdate(self, **kw):
"""Updates an existing user"""
restrict_post()
if kw.get('submit') == 'Cancel':
if kw.get('submit') == 'Cancel Edit':
turbogears.flash("Edit user cancelled")
raise turbogears.redirect('/usershow', uid=kw.get('uid'))
@ -188,7 +197,7 @@ class Root(controllers.RootController):
def userindex(self):
raise turbogears.redirect("/userlist")
@expose()
# @expose()
def generate_password(self):
password = ""
generator = random.SystemRandom()
@ -203,6 +212,9 @@ class Root(controllers.RootController):
if (len(givenname) == 0) or (len(sn) == 0):
return ""
givenname = givenname.lower()
sn = sn.lower()
uid = givenname[0] + sn[:7]
try:
client.get_user_by_uid(uid)
@ -244,6 +256,9 @@ class Root(controllers.RootController):
if (len(givenname) == 0) or (len(sn) == 0):
return ""
givenname = givenname.lower()
sn = sn.lower()
# TODO - get from config
domain = "freeipa.org"

View File

@ -3,29 +3,41 @@ from turbogears import validators, widgets
class UserFields():
uid = widgets.TextField(name="uid", label="Login")
userpassword = widgets.TextField(name="userpassword", label="Password")
userpassword = widgets.PasswordField(name="userpassword", label="Password")
userpassword_confirm = widgets.PasswordField(name="userpassword_confirm",
label="Confirm Password")
uidnumber = widgets.TextField(name="uidnumber", label="UID")
gidnumber = widgets.TextField(name="gidnumber", label="GID")
givenname = widgets.TextField(name="givenname", label="First name")
sn = widgets.TextField(name="sn", label="Last name")
mail = widgets.TextField(name="mail", label="E-mail address")
telephonenumber = widgets.TextField(name="telephonenumber", label="Phone")
nsAccountLock = widgets.CheckBox(name="nsAccountLock", label="Account Deactivated")
uid.validator = validators.PlainText(not_empty=True)
userpassword.validator = validators.String(not_empty=True)
givenname.validator = validators.String(not_empty=True)
sn.validator = validators.String(not_empty=True)
mail.validator = validators.Email(not_empty=True)
# validators.PhoneNumber may be a bit too picky, requiring an area code
telephonenumber.validator = validators.PlainText(not_empty=True)
# nsAccountLock = widgets.CheckBox(name="nsAccountLock", label="Account Deactivated")
nsAccountLock = widgets.SingleSelectField(name="nsAccountLock",
label="Account Status",
options = [("", "active"), ("true", "inactive")])
uid_hidden = widgets.HiddenField(name="uid")
uidnumber_hidden = widgets.HiddenField(name="uidnumber")
gidnumber_hidden = widgets.HiddenField(name="gidnumber")
krbPasswordExpiration_hidden = widgets.HiddenField(name="krbPasswordExpiration")
user_orig = widgets.HiddenField(name="user_orig")
class UserNewValidator(validators.Schema):
uid = validators.PlainText(not_empty=True)
userpassword = validators.String(not_empty=False)
userpassword_confirm = validators.String(not_empty=False)
givenname = validators.String(not_empty=True)
sn = validators.String(not_empty=True)
mail = validators.Email(not_empty=True)
# validators.PhoneNumber may be a bit too picky, requiring an area code
# telephonenumber = validators.PlainText(not_empty=False)
chained_validators = [
validators.FieldsMatch('userpassword', 'userpassword_confirm')
]
class UserNewForm(widgets.Form):
params = ['user']
@ -34,6 +46,8 @@ class UserNewForm(widgets.Form):
UserFields.uidnumber, UserFields.gidnumber,
UserFields.sn, UserFields.mail]
validator = UserNewValidator()
def __init__(self, *args, **kw):
super(UserNewForm,self).__init__(*args, **kw)
(self.template_c, self.template) = widgets.meta.load_kid_template("ipagui.templates.usernewform")
@ -46,6 +60,18 @@ class UserNewForm(widgets.Form):
def has_foo(self):
return False
class UserEditValidator(validators.Schema):
userpassword = validators.String(not_empty=False)
userpassword_confirm = validators.String(not_empty=False)
givenname = validators.String(not_empty=True)
sn = validators.String(not_empty=True)
mail = validators.Email(not_empty=True)
# validators.PhoneNumber may be a bit too picky, requiring an area code
# telephonenumber = validators.PlainText(not_empty=False)
chained_validators = [
validators.FieldsMatch('userpassword', 'userpassword_confirm')
]
class UserEditForm(widgets.Form):
params = ['user']
@ -53,8 +79,11 @@ class UserEditForm(widgets.Form):
fields = [UserFields.givenname, UserFields.sn, UserFields.mail,
UserFields.uid_hidden, UserFields.user_orig,
UserFields.uidnumber_hidden, UserFields.gidnumber_hidden,
UserFields.krbPasswordExpiration_hidden,
]
validator = UserEditValidator()
def __init__(self, *args, **kw):
super(UserEditForm,self).__init__(*args, **kw)
(self.template_c, self.template) = widgets.meta.load_kid_template("ipagui.templates.usereditform")

View File

@ -0,0 +1 @@
# __init__.py

View File

@ -0,0 +1,29 @@
import sys
import datetime
from ipa import ipautil
def password_expires_in(datestr):
"""Returns the number of days that password expires in. Returns a negative number
if the password is already expired."""
if (datestr == None) or (datestr == ""):
return sys.maxint
expdate = ipautil.parse_generalized_time(datestr)
if not expdate:
return sys.maxint
delta = expdate - datetime.datetime.now()
return delta.days
def password_is_expired(days):
return days < 0
def password_expires_soon(days):
return (not password_is_expired(days)) and (days < 7)
def account_status_display(status):
if status == "true":
return "inactive"
else:
return "active"

View File

@ -17,6 +17,7 @@ body {
background:#ccc; /* should be same as #sidebar */
margin:0 auto;
width:100%;
clear:both;
}
@ -24,9 +25,26 @@ body {
background:#fff;
}
#header h1 {
padding:5px;
margin:0;
#header #logo {
float:left;
}
#header #headerinfo {
text-align:right;
padding-right:10px;
}
#header #headerinfo #login {
}
#header #headerinfo #topsearch {
padding-top: 15px;
}
.searchtext {
background-color:#E5F1F4;
border:1px solid #8E8E8E;
color:#444444;
}
@ -75,7 +93,11 @@ body {
float:left;
width:10%;
padding: 5px;
font-size: small;
font-size: medium;
}
#sidebar p {
line-height: 150%;
}
#sidebar h2 {
@ -140,6 +162,12 @@ body {
font-weight: bolder;
}
.warning_message {
font-size: 120%;
color: #ee0000;
font-weight: bolder;
}
.fielderror {
color: red;
font-weight: bold;

View File

@ -24,35 +24,66 @@
</span>
</div>
<div id="page">
<div id="header">
<div id="header">
<div id="logo">
<a href="${tg.url('/')}"><img
src="${tg.url('/static/images/logo.png')}"
border="0"
/></a>
src="${tg.url('/static/images/logo.png')}"
border="0"
/></a>
</div>
<div id="headerinfo">
<div id="login">
Logged in as: ace
</div>
<div id="topsearch">
<form action="${tg.url('/topsearch')}" method="post">
<select name="searchtype">
<option>Users</option>
<option>Groups</option>
</select>
<input class="searchtext" id="topsearchbox" type="text"
name="searchvalue"
value="Type search terms here."
onfocus="clearsearch()" />
<input type="submit" value="Search"/>
</form>
<script type="text/javascript">
function clearsearch() {
topsearchbox = document.getElementById('topsearchbox');
topsearchbox.onfocus = null;
topsearchbox.value = "";
}
</script>
</div>
</div>
</div>
<div id="page">
<div id="nav"><!--
This used to have links. Keeping around in case we move them back...
--></div>
<div id="sidebar">
<h2>Tasks</h2>
<p>
<a href="${tg.url('/usernew')}">Add Person</a><br/>
<a href="${tg.url('/userlist')}">Find People</a><br/>
<br />
</p>
<p>
<a href="${tg.url('/groupindex')}">Add Group</a><br/>
<a href="${tg.url('/groupindex')}">Find Groups</a><br/>
<br />
</p>
<p>
<a href="${tg.url('/')}">Manage Policy</a><br/>
<a href="${tg.url('/')}">Self Service</a><br/>
</p>
</div>
<div py:replace="[item.text]+item[:]"></div>
<div id="footer">
<a href="http://www.freeipa.com/">Powered by FreeIPA</a>
<a href="http://www.freeipa.com/" target="_blank">Powered by FreeIPA</a>
</div>
</div>

View File

@ -8,6 +8,24 @@
<body>
<h2>Edit Person</h2>
<?python
from ipagui.helpers import userhelper
pw_expires_days = userhelper.password_expires_in(user.get("krbPasswordExpiration"))
pw_expires_soon = userhelper.password_expires_soon(pw_expires_days)
pw_is_expired = userhelper.password_is_expired(pw_expires_days)
if pw_expires_days != 1:
days_suffix = "s"
else:
days_suffix = ""
?>
<div py:if='pw_expires_soon' class="warning_message">
Password will expire in ${pw_expires_days} day${days_suffix}
</div>
<div py:if='pw_is_expired' class="warning_message">
Password has expired
</div>
${form.display(action="userupdate", value=user)}
</body>
</html>

View File

@ -56,8 +56,9 @@
<span py:replace="user.userpassword.display(value_for(user.userpassword))" />
<span py:if="tg.errors.get('userpassword')" class="fielderror"
py:content="tg.errors.get('userpassword')" />
<span id="password_text">********</span>
<!--
<span id="password_text">********</span>
<input id="genpassword_button" type="button" value="Generate Password"
disabled="true"
onclick="new Ajax.Request('${tg.url('/generate_password')}',
@ -89,6 +90,20 @@
}
}
</script>
-->
</td>
</tr>
<tr>
<th valign="top">
<label class="fieldlabel" for="${user.userpassword_confirm.field_id}"
py:content="user.userpassword_confirm.label" />:
</th>
<td valign="top">
<span py:replace="user.userpassword_confirm.display(
value_for(user.userpassword_confirm))" />
<span py:if="tg.errors.get('userpassword_confirm')" class="fielderror"
py:content="tg.errors.get('userpassword_confirm')" />
</td>
</tr>
@ -158,11 +173,13 @@
<tr>
<th>
<br />
<input type="submit" class="submitbutton" name="submit" value="Submit"/>
<input type="submit" class="submitbutton" name="submit"
value="Update Person"/>
</th>
<td>
<br />
<input type="submit" class="submitbutton" name="submit" value="Cancel" />
<input type="submit" class="submitbutton" name="submit"
value="Cancel Edit" />
</td>
<td></td>
</tr>

View File

@ -6,20 +6,18 @@
<title>Find People</title>
</head>
<body>
<h2>Find People</h2>
<div id="search">
<form action="${tg.url('/userlist')}" method="post">
Search:
<input id="uid" type="text" name="uid" value="${uid}" />
<input type="submit" />
<input type="submit" value="Find People"/>
</form>
<script type="text/javascript">
document.getElementById("uid").focus();
</script>
</div>
<div py:if='users != None'>
<div py:if='(users != None) and (len(users) > 0)'>
<h2>${len(users)} results returned:</h2>
<table id="resultstable" py:if='len(users) > 0'>
<table id="resultstable">
<tr>
<th>
<label class="fieldlabel" py:content="fields.uid.label" />
@ -61,9 +59,9 @@
</td>
</tr>
</table>
<div py:if='len(users) == 0'>
No results found.
</div>
</div>
<div py:if='(users != None) and (len(users) == 0)'>
<h2>No results found for "${uid}"</h2>
</div>
</body>
</html>

View File

@ -27,6 +27,9 @@
<span py:if="tg.errors.get('sn')" class="fielderror"
py:content="tg.errors.get('sn')" />
<script type="text/javascript">
var uid_suggest = ""
var mail_suggest = ""
function autofill(self) {
givenname = document.getElementById('form_givenname');
sn = document.getElementById('form_sn');
@ -35,22 +38,24 @@
}
uid = document.getElementById('form_uid');
mail = document.getElementById('form_mail');
if (uid.value == "") {
if ((uid.value == "") || (uid.value == uid_suggest)) {
new Ajax.Request('${tg.url('/suggest_uid')}', {
method: 'get',
parameters: {'givenname': givenname.value, 'sn': sn.value},
onSuccess: function(transport) {
uid.value = transport.responseText;
uid_suggest = uid.value;
new Effect.Highlight(uid);
}
});
}
if (mail.value == "") {
if ((mail.value == "") || (mail.value == mail_suggest)) {
new Ajax.Request('${tg.url('/suggest_email')}', {
method: 'get',
parameters: {'givenname': givenname.value, 'sn': sn.value},
onSuccess: function(transport) {
mail.value = transport.responseText;
mail_suggest = mail.value;
new Effect.Highlight(mail);
}
});
@ -87,6 +92,7 @@
<span py:if="tg.errors.get('userpassword')" class="fielderror"
py:content="tg.errors.get('userpassword')" />
<!--
<input type="button" value="Generate Password"
onclick="new Ajax.Request('${tg.url('/generate_password')}',
{
@ -96,6 +102,20 @@
transport.responseText;
}
});" />
-->
</td>
</tr>
<tr>
<th>
<label class="fieldlabel" for="${user.userpassword_confirm.field_id}"
py:content="user.userpassword_confirm.label" />:
</th>
<td>
<span py:replace="user.userpassword_confirm.display(
value_for(user.userpassword_confirm))" />
<span py:if="tg.errors.get('userpassword_confirm')" class="fielderror"
py:content="tg.errors.get('userpassword_confirm')" />
</td>
</tr>
@ -167,15 +187,11 @@
<table class="formtable" cellpadding="2" cellspacing="0" border="0">
<tr>
<th>
<br />
<input type="submit" class="submitbutton" name="submit" value="Submit"/>
</th>
<th></th>
<td>
<br />
<input type="submit" class="submitbutton" name="submit" value="Cancel" />
<input type="submit" class="submitbutton" name="submit" value="Add Person"/>
</td>
<td></td>
</tr>
</table>

View File

@ -8,6 +8,24 @@
<body>
<h2>View Person</h2>
<?python
from ipagui.helpers import userhelper
pw_expires_days = userhelper.password_expires_in(user.get("krbPasswordExpiration"))
pw_expires_soon = userhelper.password_expires_soon(pw_expires_days)
pw_is_expired = userhelper.password_is_expired(pw_expires_days)
if pw_expires_days != 1:
days_suffix = "s"
else:
days_suffix = ""
?>
<div py:if='pw_expires_soon' class="warning_message">
Password will expire in ${pw_expires_days} day${days_suffix}
</div>
<div py:if='pw_is_expired' class="warning_message">
Password has expired
</div>
<div class="formsection">Identity Details</div>
<table class="formtable" cellpadding="2" cellspacing="0" border="0">
<tr>
@ -68,7 +86,7 @@
<th>
<label class="fieldlabel" py:content="fields.nsAccountLock.label" />:
</th>
<td>${user.get("nsAccountLock")}</td>
<td>${userhelper.account_status_display(user.get("nsAccountLock"))}</td>
</tr>
</table>

View File

@ -2,7 +2,8 @@
Required packages:
krb5-server
fedora-ds-base / fedora-ds-base-devel
fedora-ds-base
fedora-ds-base-devel
openldap-clients
krb5-server-ldap
cyrus-sasl-gssapi
@ -13,12 +14,40 @@ openssl-devel
Installation example:
TEMPORARY: (until fedora ds scripts are fixed)
please use the fedora-ds.init.patch under share/ to patch your init scripts before
running ipa-server-install
TEMPORARY: until bug https://bugzilla.redhat.com/show_bug.cgi?id=248169 is
fixed.
cd ipa-install
make install
cd ..
/usr/sbin/ipa-server-install -u fds -r FREEIPA.ORG -p freeipa -m ipafree
Please apply the fedora-ds.init.patch in freeipa/ipa-server/ipa-install/share/
to patch your init scripts before running ipa-server-install. This tells
FDS where to find its kerberos keytab.
Things done as root are denoted by #. Things done as a unix user are denoted
by %.
# cd freeipa
# patch -p0 < ipa-server/ipa-install/share/fedora-ds.init.patch
Now to do the installation.
# cd freeipa
# make install
# /usr/sbin/ipa-server-install -u fds -r FREEIPA.ORG -p freeipa -P ipafree
For more verbose output add the -d flag
You have a basic working system with one super administrator (named admin).
To create another administrative user:
% kinit admin@FREEIPA.ORG
% /usr/sbin/ipa-adduser -f Test -l User test
% ldappasswd -Y GSSAPI -h localhost -s password uid=test,cn=users,cn=accounts,dc=freeipa,dc=org
% /usr/sbin/ipa-groupmod -a test admins
An admin user is just a regular user in the group admin.
Now you can destroy the old ticket and log in as test:
% kdestroy
% kinit test@FREEIPA.ORG
% /usr/sbin/ipa-finduser test

View File

@ -1,4 +1,4 @@
SUBDIRS=ipa-pwd-extop ipa-memberof
SUBDIRS=ipa-pwd-extop ipa-memberof dna
all:
@for subdir in $(SUBDIRS); do \

View File

@ -0,0 +1,34 @@
DIRSRV ?= dirsrv
PREFIX ?= $(DESTDIR)/usr
LIBDIR ?= $(PREFIX)/lib/$(DIRSRV)/plugins
LIB64DIR ?= $(PREFIX)/lib64/$(DIRSRV)/plugins
SHAREDIR = $(DESTDIR)/usr/share/ipa
SONAME = libipa-dna-plugin.so
LDFLAGS += -llber
CFLAGS ?= -g -Wall -Wshadow
CFLAGS += -I/usr/include/$(DIRSRV) -I/usr/include/nss3 -I/usr/include/mozldap -I/usr/include/nspr4 -fPIC -DPIC
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
all: $(OBJS)
$(CC) $(LDFLAGS) $(OBJS) -Wl,-soname -Wl,$(SONAME) -shared -o $(SONAME)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
install:
-mkdir -p $(LIBDIR)
if [ -e $(PREFIX)/lib/$(DIRSRV) ]; then \
install -m 644 $(SONAME) $(LIBDIR); \
else \
install -m 644 $(SONAME) $(LIB64DIR); \
fi
install -m 644 *.ldif $(SHAREDIR)
clean:
rm -f *.o
rm -f $(SONAME)
rm -f *~

View File

@ -0,0 +1,13 @@
dn: cn=ipa-dna,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
objectclass: extensibleObject
cn: ipa-dna
nsslapd-pluginpath: libipa-dna-plugin
nsslapd-plugininitfunc: dna_init
nsslapd-plugintype: postoperation
nsslapd-pluginenabled: on
nsslapd-pluginid: ipa-dna
nsslapd-pluginversion: 1.0
nsslapd-pluginvendor: Red Hat
nsslapd-plugindescription: Distributed numeric assignment plugin

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ import tempfile
import shutil
import logging
import pwd
from util import *
from ipa.ipautil import *
SHARE_DIR = "/usr/share/ipa/"

View File

@ -35,13 +35,14 @@ import cStringIO
import time
import operator
import struct
import ldap.sasl
from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples
from ldap.modlist import modifyModlist
from ldap.ldapobject import SimpleLDAPObject
from ipa import ipaerror, ipautil
# Global variable to define SASL auth
sasl_auth = ldap.sasl.sasl({},'GSSAPI')
class Entry:
"""This class represents an LDAP Entry object. An LDAP entry consists of a DN
and a list of attributes. Each attribute consists of a name and a list of
@ -196,22 +197,34 @@ class IPAdmin(SimpleLDAPObject):
raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
def __localinit__(self):
SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port))
"""If a CA certificate is provided then it is assumed that we are
doing SSL client authentication with proxy auth.
If a CA certificate is not present then it is assumed that we are
using a forwarded kerberos ticket for SASL auth. SASL provides
its own encryption.
"""
if self.cacert is not None:
SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port))
else:
SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port))
def __init__(self,host,port,cacert,bindcert,bindkey,proxydn=None):
"""We just set our instance variables and wrap the methods - the real work is
done in __localinit__ and __initPart2 - these are separated out this way so
that we can call them from places other than instance creation e.g. when
using the start command, we just need to reconnect, not create a new instance"""
"""We just set our instance variables and wrap the methods - the real
work is done in __localinit__ and __initPart2 - these are separated
out this way so that we can call them from places other than
instance creation e.g. when we just need to reconnect, not create a
new instance"""
# ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
if cacert is not None:
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
self.__wrapmethods()
self.port = port or 389
self.sslport = 0
self.host = host
self.cacert = cacert
self.bindcert = bindcert
self.bindkey = bindkey
self.proxydn = proxydn
@ -251,6 +264,12 @@ class IPAdmin(SimpleLDAPObject):
def set_proxydn(self, proxydn):
self.proxydn = proxydn
def set_keytab(self, keytab):
if keytab is not None:
os.environ["KRB5CCNAME"] = keytab
self.sasl_interactive_bind_s("", sasl_auth)
self.proxydn = None
def getEntry(self,*args):
"""This wraps the search function. It is common to just get one entry"""
@ -346,7 +365,8 @@ class IPAdmin(SimpleLDAPObject):
sctrl = self.__get_server_controls__()
try:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
if sctrl is not None:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
self.add_s(*args)
except ldap.ALREADY_EXISTS:
raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
@ -366,7 +386,8 @@ class IPAdmin(SimpleLDAPObject):
raise ipaerror.gen_exception(ipaerror.LDAP_EMPTY_MODLIST)
try:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
if sctrl is not None:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
self.modify_s(dn, modlist)
# this is raised when a 'delete' attribute isn't found.
# it indicates the previous attribute was removed by another
@ -428,7 +449,8 @@ class IPAdmin(SimpleLDAPObject):
modlist.append((operation, "nsAccountlock", "true"))
try:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
if sctrl is not None:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
self.modify_s(dn, modlist)
except ldap.LDAPError, e:
raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
@ -440,7 +462,8 @@ class IPAdmin(SimpleLDAPObject):
sctrl = self.__get_server_controls__()
try:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
if sctrl is not None:
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
self.delete_s(*args)
except ldap.LDAPError, e:
raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)

View File

@ -29,7 +29,7 @@ import os
import pwd
import socket
import time
from util import *
from ipa.ipautil import *
def host_to_domain(fqdn):
s = fqdn.split(".")

View File

@ -47,20 +47,31 @@ DefaultGroupContainer = "cn=groups,cn=accounts"
# this is not anticipated.
class IPAConnPool:
def __init__(self):
self.numentries = 0
self.freelist = []
def getConn(self, host, port, bindca, bindcert, bindkey, proxydn=None):
self.numentries = self.numentries + 1
def getConn(self, host, port, bindca, bindcert, bindkey, proxydn=None, keytab=None):
conn = None
if len(self.freelist) > 0:
conn = self.freelist.pop()
else:
for i in range(len(self.freelist)):
c = self.freelist[i]
if ((c.host == host) and (c.port == port)):
conn = self.freelist.pop(i)
break
if conn is None:
conn = ipaserver.ipaldap.IPAdmin(host,port,bindca,bindcert,bindkey)
conn.set_proxydn(proxydn)
if proxydn is not None:
conn.set_proxydn(proxydn)
else:
conn.set_keytab(keytab)
return conn
def releaseConn(self, conn):
self.freelist.append(conn)
# We can't re-use SASL connections. If proxydn is None it means
# we have a keytab set. See ipaldap.set_keytab
if conn.proxydn is None:
conn.unbind_s()
else:
self.freelist.append(conn)
class IPAServer:
@ -68,7 +79,8 @@ class IPAServer:
global _LDAPPool
# FIXME, this needs to be auto-discovered
self.host = 'localhost'
self.port = 636
self.port = 389
self.sslport = 636
self.bindcert = "/usr/share/ipa/cert.pem"
self.bindkey = "/usr/share/ipa/key.pem"
self.bindca = "/usr/share/ipa/cacert.asc"
@ -79,24 +91,84 @@ class IPAServer:
self.basedn = ipa.ipautil.realm_to_suffix(ipa.config.config.get_realm())
self.scope = ldap.SCOPE_SUBTREE
self.princ = None
self.keytab = None
def set_principal(self, princ):
self.princ = princ
def set_keytab(self, keytab):
self.keytab = keytab
def get_dn_from_principal(self, princ):
"""Given a kerberls principal get the LDAP uid"""
"""Given a kerberos principal get the LDAP uid"""
global _LDAPPool
filter = "(krbPrincipalName=" + princ + ")"
# The only anonymous search we should have
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,None)
conn = _LDAPPool.getConn(self.host,self.sslport,self.bindca,self.bindcert,self.bindkey,None,None)
try:
ent = m1.getEntry(self.basedn, self.scope, filter, ['dn'])
ent = conn.getEntry(self.basedn, self.scope, filter, ['dn'])
finally:
_LDAPPool.releaseConn(m1)
_LDAPPool.releaseConn(conn)
return "dn:" + ent.dn
def __setup_connection(self, opts):
"""Set up common things done in the connection.
If there is a keytab then return None as the proxy dn and the keytab
otherwise return the proxy dn and None as the keytab.
We only want one or the other used at one time and we prefer
the keytab. So if there is a keytab, return that and None for
proxy dn to make calling getConn() easier.
"""
if opts:
if opts.get('keytab'):
self.set_keytab(opts['keytab'])
self.set_principal(None)
else:
self.set_keytab(None)
self.set_principal(opts['remoteuser'])
else:
self.set_keytab(None)
# The caller should have already set the principal
if self.princ is not None:
return self.get_dn_from_principal(self.princ), None
else:
return None, self.keytab
def getConnection(self, opts):
"""Wrapper around IPAConnPool.getConn() so we don't have to pass
around self.* every time a connection is needed.
For SASL connections (where we have a keytab) we can't set
the SSL variables for certificates. It confuses the ldap
module.
"""
global _LDAPPool
(proxy_dn, keytab) = self.__setup_connection(opts)
if keytab is not None:
bindca = None
bindcert = None
bindkey = None
port = self.port
else:
bindca = self.bindca
bindcert = self.bindcert
bindkey = self.bindkey
port = self.sslport
return _LDAPPool.getConn(self.host,port,bindca,bindcert,bindkey,proxy_dn,keytab)
def releaseConnection(self, conn):
global _LDAPPool
_LDAPPool.releaseConn(conn)
def convert_entry(self, ent):
entry = dict(ent.data)
entry['dn'] = ent.dn
@ -110,24 +182,17 @@ class IPAServer:
entry[key] = value[0]
return entry
def __get_entry (self, base, filter, sattrs=None, opts=None):
"""Get a specific entry. Return as a dict of values.
Multi-valued fields are represented as lists.
"""
global _LDAPPool
ent=""
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
ent = m1.getEntry(base, self.scope, filter, sattrs)
ent = conn.getEntry(base, self.scope, filter, sattrs)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return self.convert_entry(ent)
@ -137,8 +202,6 @@ class IPAServer:
oldentry is a dict
newentry is a dict
"""
global _LDAPPool
oldentry = self.convert_scalar_values(oldentry)
newentry = self.convert_scalar_values(newentry)
@ -150,16 +213,11 @@ class IPAServer:
except KeyError, e:
raise ipaerror.gen_exception(ipaerror.LDAP_MISSING_DN)
if opts:
self.set_principal(opts['remoteuser'])
proxydn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn)
conn = self.getConnection(opts)
try:
res = m1.updateEntry(moddn, oldentry, newentry)
res = conn.updateEntry(moddn, oldentry, newentry)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return res
def __safe_filter(self, criteria):
@ -234,8 +292,6 @@ class IPAServer:
attribute name and the value is either a string or in the case
of a multi-valued field a list of values. user_container sets
where in the tree the user is placed."""
global _LDAPPool
if user_container is None:
user_container = DefaultUserContainer
@ -288,16 +344,11 @@ class IPAServer:
for u in user:
entry.setValues(u, user[u])
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
res = m1.addEntry(entry)
res = conn.addEntry(entry)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return res
def get_add_schema (self):
@ -348,20 +399,13 @@ class IPAServer:
"""Return a list containing a User object for each
existing user.
"""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
filter = "(objectclass=posixAccount)"
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
all_users = m1.getList(self.basedn, self.scope, filter, None)
all_users = conn.getList(self.basedn, self.scope, filter, None)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
users = []
for u in all_users:
@ -372,13 +416,6 @@ class IPAServer:
def find_users (self, criteria, sattrs=None, opts=None):
"""Returns a list: counter followed by the results.
If the results are truncated, counter will be set to -1."""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
# Assume the list of fields to search will come from a central
# configuration repository. A good format for that would be
# a comma-separated list of fields
@ -389,26 +426,26 @@ class IPAServer:
criteria_words = re.split(r'\s+', criteria)
criteria_words = filter(lambda value:value!="", criteria_words)
if len(criteria_words) == 0:
return []
return [0]
(exact_match_filter, partial_match_filter) = self.__generate_match_filters(
search_fields, criteria_words)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
try:
exact_results = m1.getListAsync(self.basedn, self.scope,
exact_results = conn.getListAsync(self.basedn, self.scope,
exact_match_filter, sattrs)
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
exact_results = [0]
try:
partial_results = m1.getListAsync(self.basedn, self.scope,
partial_results = conn.getListAsync(self.basedn, self.scope,
partial_match_filter, sattrs)
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
partial_results = [0]
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
exact_counter = exact_results[0]
partial_counter = partial_results[0]
@ -450,13 +487,6 @@ class IPAServer:
def mark_user_deleted (self, uid, opts=None):
"""Mark a user as inactive in LDAP. We aren't actually deleting
users here, just making it so they can't log in, etc."""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
proxydn = self.get_dn_from_principal(self.princ)
user = self.get_user_by_uid(uid, ['dn', 'uid', 'nsAccountlock'], opts)
# Are we doing an add or replace operation?
@ -467,11 +497,11 @@ class IPAServer:
else:
has_key = False
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn)
conn = self.getConnection(opts)
try:
res = m1.inactivateEntry(user['dn'], has_key)
res = conn.inactivateEntry(user['dn'], has_key)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return res
def delete_user (self, uid, opts=None):
@ -483,18 +513,15 @@ class IPAServer:
The memberOf plugin handles removing the user from any other
groups.
"""
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
user_dn = self.get_user_by_uid(uid, ['dn', 'uid', 'objectclass'], opts)
if user_dn is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
res = m1.deleteEntry(user_dn['dn'])
_LDAPPool.releaseConn(m1)
conn = self.getConnection(opts)
try:
res = conn.deleteEntry(user_dn['dn'])
finally:
self.releaseConnection(conn)
return res
# Group support
@ -532,8 +559,6 @@ class IPAServer:
attribute name and the value is either a string or in the case
of a multi-valued field a list of values. group_container sets
where in the tree the group is placed."""
global _LDAPPool
if group_container is None:
group_container = DefaultGroupContainer
@ -554,38 +579,26 @@ class IPAServer:
for g in group:
entry.setValues(g, group[g])
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
res = m1.addEntry(entry)
res = conn.addEntry(entry)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
def find_groups (self, criteria, sattrs=None, opts=None):
"""Return a list containing a User object for each
existing group that matches the criteria.
"""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
criteria = self.__safe_filter(criteria)
filter = "(&(cn=%s)(objectClass=posixGroup))" % criteria
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
results = m1.getList(self.basedn, self.scope, filter, sattrs)
results = conn.getList(self.basedn, self.scope, filter, sattrs)
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
results = []
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
groups = []
for u in results:
@ -599,9 +612,6 @@ class IPAServer:
group is the cn of the group to be added to
"""
if opts:
self.set_principal(opts['remoteuser'])
old_group = self.get_group_by_cn(group, None, opts)
if old_group is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
@ -652,9 +662,6 @@ class IPAServer:
group is the cn of the group to be removed from
"""
if opts:
self.set_principal(opts['remoteuser'])
old_group = self.get_group_by_cn(group, None, opts)
if old_group is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
@ -718,19 +725,16 @@ class IPAServer:
The memberOf plugin handles removing the group from any other
groups.
"""
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
group = self.get_group_by_cn(group_cn, ['dn', 'cn'], opts)
if len(group) != 1:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
res = m1.deleteEntry(group[0]['dn'])
_LDAPPool.releaseConn(m1)
conn = self.getConnection(opts)
try:
res = conn.deleteEntry(group[0]['dn'])
finally:
self.releaseConnection(conn)
return res
def add_group_to_group(self, group, tgroup, opts=None):
@ -739,9 +743,6 @@ class IPAServer:
tgroup is the cn of the group to be added to
"""
if opts:
self.set_principal(opts['remoteuser'])
old_group = self.get_group_by_cn(tgroup, None, opts)
if old_group is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)

View File

@ -126,13 +126,19 @@ class ModXMLRPCRequestHandler(object):
def register_instance(self,instance):
self.register_module(instance)
def _marshaled_dispatch(self, data, remoteuser):
def _marshaled_dispatch(self, data, req):
"""Dispatches an XML-RPC method from marshalled (XML) data."""
params, method = loads(data)
# Populate the Apache environment variables
req.add_common_vars()
opts={}
opts['remoteuser'] = remoteuser
opts['remoteuser'] = req.user
if req.subprocess_env.get("KRB5CCNAME") is not None:
opts['keytab'] = req.subprocess_env.get("KRB5CCNAME")
# Tack onto the end of the passed-in arguments any options we also
# need
@ -263,7 +269,7 @@ class ModXMLRPCRequestHandler(object):
req.allow_methods(['POST'],1)
raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED
response = self._marshaled_dispatch(req.read(), req.user)
response = self._marshaled_dispatch(req.read(), req)
req.content_type = "text/xml"
req.set_content_length(len(response))