--- a/.hgignore Tue Jan 08 23:21:25 2013 -0500
+++ b/.hgignore Fri Jan 11 00:32:54 2013 -0500
@@ -1,3 +1,4 @@
^packages/.*/.*\.deb$
^tarballs/
+^packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/compiled/
~$
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/.htaccess Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/.htaccess Fri Jan 11 00:32:54 2013 -0500
@@ -5,8 +5,10 @@
AuthDBMGroupFile /etc/apache2/ldap-groups
Require group rtp
-<FilesMatch "^logout(\.php)?$">
+<FilesMatch "^((webauth-)?logout|lostpw|pw-strength)(\.php)?$">
Require valid-user
+ Satisfy Any
+ AuthType None
</FilesMatch>
RewriteEngine on
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/img/.htaccess Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,3 @@
+AuthType None
+Satisfy Any
+Require valid-user
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/functions.php Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/functions.php Fri Jan 11 00:32:54 2013 -0500
@@ -29,7 +29,7 @@
function load_credentials()
{
$config = yaml_parse_file("/usr/local/etc/ssoinabox/webcreds.yml");
- $keys = array('LDAP_BASEDN', 'UID_MIN', 'GID_MIN', 'ldap_server', 'ldap_manager', 'ldap_user_basedn', 'ldap_group_basedn', 'kerberos_admin', 'PHONE_EXT_MIN');
+ $keys = array('LDAP_BASEDN', 'UID_MIN', 'GID_MIN', 'ldap_server', 'ldap_manager', 'ldap_user_basedn', 'ldap_group_basedn', 'kerberos_admin', 'PHONE_EXT_MIN', 'hmac_secret');
foreach ( $keys as $key )
{
@@ -42,3 +42,48 @@
$GLOBALS[$key] = $config[$key];
}
}
+
+/**
+ * Test a password's policy compliance
+ * @param string password
+ * @return mixed true if compliant, otherwise a string describing why it isn't
+ */
+
+function test_password($str)
+{
+ if ( strlen($str) < 8 )
+ return 'must be at least 8 characters in length';
+
+ if ( countUniqueChars($str) < 6 )
+ return 'must have at least 6 unique characters';
+
+ if ( strlen($str) <= 16 )
+ {
+ if ( !preg_match('/[a-z]/', $str) )
+ return 'must contain at least one lowercase letter';
+
+ if ( !preg_match('/[A-Z]/', $str) )
+ return 'must contain at least one lowercase letter';
+
+ if ( !preg_match('/[0-9]/', $str) )
+ return 'must contain at least one lowercase letter';
+
+ if ( !preg_match('/[^A-Za-z0-9]/', $str) )
+ return 'must contain at least one lowercase letter';
+ }
+
+ return true;
+}
+
+function countUniqueChars($str)
+{
+ $count = 0;
+ $uniq = '';
+ for ( $i = 0; $i < strlen($str); $i++ )
+ {
+ if ( strpos($uniq, $str{$i}) === false )
+ $uniq .= $str{$i};
+ }
+
+ return strlen($uniq);
+}
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/ldap.php Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/ldap.php Fri Jan 11 00:32:54 2013 -0500
@@ -353,3 +353,20 @@
return ldap_delete($_ldapconn, ldap_make_group_dn($cn));
}
+
+/**
+ * Is the given username in the specified LDAP group?
+ * @param string username
+ * @param string Group name
+ * @return bool
+ */
+
+function ldap_test_group_membership($username, $group)
+{
+ global $_ldapconn, $ldap_group_basedn;
+
+ $filter = sprintf('(&(memberUid=%s)(cn=%s)(objectClass=posixGroup))', ldap_escape($username), ldap_escape($group));
+
+ $result = ldap_search($_ldapconn, $ldap_group_basedn, $filter);
+ return ldap_count_entries($_ldapconn, $result) > 0;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/smtp.php Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * Sends e-mail via SMTP.
+ * @param string Source address
+ * @param string Destination address
+ * @param string Subject
+ * @param string Message body
+ * @param string Additional headers
+ * @return bool
+ */
+
+function smtp_mail($from, $to, $subject, $body, $headers = '')
+{
+ list($domain) = array_reverse(explode('@', $to));
+
+ // look up MX record for $domain
+ $record = dns_get_record($domain, DNS_MX);
+
+ if ( !isset($record[0]['target']) )
+ // failed to get target server
+ return false;
+
+ // open socket
+ $sock = fsockopen($record[0]['target'], 25, $errno, $errstr, 5);
+ if ( !$sock )
+ // failed to open socket
+ return false;
+
+ try
+ {
+ // wait for 220
+ if ( _smtp_get_response($sock) !== 220 )
+ throw new Exception("Expected 220");
+
+ // HELO
+ // get server's FQDN
+ $hostname = gethostname();
+ if ( strpos($hostname, '.') === false )
+ {
+ $hostname .= '.' . trim(`dnsdomainname`);
+ }
+ fputs($sock, "HELO $hostname\r\n");
+ if ( _smtp_get_response($sock) !== 250 )
+ throw new Exception("Expected 250");
+
+ // from
+ fputs($sock, "MAIL FROM: <$from>\r\n");
+ if ( _smtp_get_response($sock) !== 250 )
+ throw new Exception("Expected 250");
+
+ // to
+ fputs($sock, "RCPT TO: <$to>\r\n");
+ if ( _smtp_get_response($sock) !== 250 )
+ throw new Exception("Expected 250");
+
+ // data
+ fputs($sock, "DATA\r\n");
+ if ( !in_array(_smtp_get_response($sock), array(354, 250)) )
+ throw new Exception("Expected 250 or 354");
+
+ // send headers
+ $full_headers = "Subject: $subject\r\n";
+ $full_headers .= "From: <$from>\r\n";
+ if ( !strstr($headers, "To: ") )
+ $full_headers .= "To: <$to>\r\n";
+ if ( !empty($headers) )
+ $full_headers .= trim(str_replace("\n", "\r\n", str_replace("\r\n", "\n", $headers))) . "\r\n";
+
+ $full_headers .= "\r\n";
+ fputs($sock, $full_headers);
+
+ // send body
+ $body = str_replace("\n", "\r\n", str_replace("\r\n", "\n", $body));
+ fputs($sock, $body);
+
+ // send end marker
+ fputs($sock, "\r\n.\r\n");
+ if ( _smtp_get_response($sock) !== 250 )
+ throw new Exception("Expected 250");
+
+ // end session
+ fputs($sock, "QUIT\r\n");
+ if ( _smtp_get_response($sock) !== 221 )
+ throw new Exception("Expected 221");
+
+ fclose($sock);
+ } catch ( Exception $e )
+ {
+ fputs($sock, "QUIT\r\n");
+ _smtp_get_response($sock);
+ fclose($sock);
+ return false;
+ }
+ return true;
+}
+
+function _smtp_get_response($sock)
+{
+ while ( !feof($sock) && ($line = fgets($sock, 8192)) )
+ {
+ if ( preg_match('/^([0-9]+)(\s.+)?$/', $line, $match) )
+ {
+ return intval($match[1]);
+ }
+ }
+ return false;
+}
+
+// smtp_mail('plates@csh.rit.edu', 'plates@csh.rit.edu', 'Test e-mail', 'Testing', 'From: Plates <plates@csh.rit.edu>');
+
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/starthere.php Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/starthere.php Fri Jan 11 00:32:54 2013 -0500
@@ -15,8 +15,12 @@
require_once(ACCOUNTS . 'includes/ldap.php');
require_once(ACCOUNTS . 'includes/kadm5.php');
require_once(ACCOUNTS . 'includes/users.php');
+require_once(ACCOUNTS . 'includes/smtp.php');
session_start();
+$adm = !empty($_SERVER['REMOTE_USER']) && ldap_test_group_membership($_SERVER['REMOTE_USER'], 'rtp');
+define('IS_ADMIN', $adm);
+
if ( !isset($_SESSION['messages']) )
$_SESSION['messages'] = array();
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/template-wrapper.php Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/template-wrapper.php Fri Jan 11 00:32:54 2013 -0500
@@ -15,6 +15,8 @@
if ( isset($_SERVER['REMOTE_USER']) )
$smarty->assign('userinfo', $ui = ldap_get_user($_SERVER['REMOTE_USER']));
+ $smarty->assign('is_admin', IS_ADMIN);
+
if ( $ui === false )
redirect('/logout');
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/emails/lostpw.tpl Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,9 @@
+Hello {$userinfo['cn']},
+
+You're receiving this e-mail because someone (hopefully you) requested a password reset on your account. To reset your password, please click the link below:
+
+{$reset_link}
+
+This request was received from the IP address {$ip}. If you did not request this e-mail, you may safely ignore it as the link will expire 12 hours from this e-mail's issuance.
+
+If the name you were greeted with above does not match your real name, please report this e-mail to the administrators and do not click any links in it.
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/header.tpl Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/header.tpl Fri Jan 11 00:32:54 2013 -0500
@@ -15,10 +15,17 @@
<div class="navbar-inner">
<a class="brand" href="/"><span class="siab-logo" title="SSO in a Box">SiaB</span> Control Panel</a>
<ul class="nav">
- <li><a href="/">Home</a></li>
+ {if $user}
+ <li><a href="/">Home</a></li>
+ {else}
+ <li><a href="/lostpw">Password recovery</a></li>
+ {/if}
+ {if $is_admin}
<li><a href="/users">Users</a></li>
<li><a href="/groups">Groups</a></li>
+ {/if}
</ul>
+ {if $user}
<div class="pull-right">
<ul class="nav">
<li>
@@ -33,6 +40,7 @@
</li>
</ul>
</div>
+ {/if}
</div>
</div>
<div class="main container">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/lostpw.tpl Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,22 @@
+{assign var="title" value="Lost Password"}
+{include file="header.tpl"}
+
+<h1>Forgot password</h1>
+
+<p>If you've forgotten your password but set an e-mail address for yourself, you have the option of recovering your account via a link sent to your e-mail address. To
+ get started, enter either your username or e-mail address in the form below.</p>
+
+<form method="post" class="form-horizontal">
+ <div class="control-group">
+ <label class="control-label">E-mail address or username:</label>
+ <div class="controls">
+ <input type="text" name="email_or_username" class="input-xlarge" />
+ </div>
+ </div>
+
+ <div class="form-actions">
+ <input type="submit" value="Send recovery e-mail" class="btn btn-primary" />
+ </div>
+</form>
+
+{include file="footer.tpl"}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/resetpw.tpl Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,42 @@
+{assign var="title" value="Lost Password"}
+{include file="header.tpl"}
+
+<h1>Reset password</h1>
+
+<p>Almost done! Complete the form below to reset your password.</p>
+
+<form method="post" class="form-horizontal">
+
+ <!-- Username -->
+ <div class="control-group">
+ <label class="control-label">Username:</label>
+ <div class="controls">
+ <input type="text" readonly="readonly" name="uid" value="{$userinfo['uid']|escape:'html'}" />
+ </div>
+ </div>
+
+ <!-- Password -->
+ <div class="control-group">
+ <label class="control-label">New password:</label>
+ <div class="controls">
+ <input type="password" name="password" />
+ <p class="help-block">
+ Must meet <a href="/pw-strength" onclick="window.open(this.href); return false;">password security requirements</a>.
+ </p>
+ </div>
+ </div>
+
+ <!-- Confirm password -->
+ <div class="control-group">
+ <label class="control-label">Confirm password:</label>
+ <div class="controls">
+ <input type="password" name="password_confirm" />
+ </div>
+ </div>
+
+ <div class="form-actions">
+ <input type="submit" value="Reset password" class="btn btn-primary" />
+ </div>
+</form>
+
+{include file="footer.tpl"}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/logout.php Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,3 @@
+<?php
+require('includes/starthere.php');
+redirect(sprintf('http://%s/webauth-logout', gethostname()));
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/lostpw.php Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,135 @@
+<?php
+
+require('includes/starthere.php');
+
+if ( !empty($_SERVER['PATH_INFO']) )
+{
+ if ( count($params = explode('/', $_SERVER['PATH_INFO'])) !== 4 )
+ {
+ redirect('/lostpw');
+ }
+
+ list(,$uid,$timestamp,$token) = $params;
+
+ try
+ {
+ $do_redirect = true;
+ if ( !preg_match('/^[a-z0-9]{3,32}$/', $uid) ||
+ !preg_match('/^[a-f0-9]{8}$/', $timestamp) ||
+ !preg_match('/^[a-f0-9]{40}$/', $token) )
+ throw new Exception("Request format is invalid");
+
+ $ts_dec = hexdec($timestamp);
+
+ if ( hash_hmac('sha1', "$uid%$ts_dec", $hmac_secret) !== $token )
+ throw new Exception("Request token is invalid");
+
+ if ( abs(time() - $ts_dec) > (12 * 3600) )
+ throw new Exception("This link has expired. Please request another password reset.");
+
+ $userinfo = ldap_get_user($uid);
+
+ // handle a submit?
+ $do_redirect = false;
+ if ( !empty($_POST['password']) )
+ {
+ if ( ($result = test_password($_POST['password'])) !== true )
+ throw new Exception("Your new password $result.");
+
+ if ( $_POST['password'] !== $_POST['password_confirm'] )
+ throw new Exception("The passwords you entered did not match.");
+
+ if ( reset_password($uid, $_POST['password']) )
+ {
+ queue_message(E_NOTICE, "Your password has been reset. You can now log into the control panel.");
+ redirect('/lostpw');
+ }
+ else
+ {
+ throw new Exception("Internal error when performing password reset.");
+ }
+ }
+ }
+ catch ( Exception $e )
+ {
+ queue_message(E_ERROR, $e->getMessage());
+ if ( $do_redirect )
+ redirect('/lostpw');
+ }
+
+ display_template('resetpw', array(
+ 'userinfo' => $userinfo
+ ));
+
+ exit;
+}
+
+if ( !empty($_POST['email_or_username']) )
+{
+ try
+ {
+ global $_ldapconn, $ldap_user_basedn;
+
+ $field = strstr($_POST['email_or_username'], '@') ? 'mail' : 'uid';
+ if ( $field == 'uid' && !preg_match('/^[a-z0-9]{3,32}$/', $_POST['email_or_username']) )
+ {
+ throw new Exception("Invalid username. Usernames can only contain 3-32 a-z and 0-9 (all lowercase) characters.");
+ }
+
+ $search_filter = sprintf('(&(%s=%s)(objectClass=posixAccount))', $field, ldap_escape($_POST['email_or_username']));
+ $search_result = ldap_search($_ldapconn, $ldap_user_basedn, $search_filter);
+
+ if ( !$search_result )
+ {
+ throw new Exception(ldap_error($_ldapconn));
+ }
+
+ if ( ldap_count_entries($_ldapconn, $search_result) == 0 )
+ {
+ throw new Exception("Could not find any accounts that matched that $field.");
+ }
+ else if ( ldap_count_entries($_ldapconn, $search_result) > 1 )
+ {
+ throw new Exception("LDAP search query erroneously returned multiple results.");
+ }
+
+ $userinfo = ldap_array_cleanup(ldap_get_attributes($_ldapconn, ldap_first_entry($_ldapconn, $search_result)));
+
+ if ( !isset($userinfo['mail']) )
+ {
+ throw new Exception("No e-mail address is registered to your account. Please contact the administrator to request a password reset.");
+ }
+
+ $mail_censored = $mail = is_array($userinfo['mail']) ? $userinfo['mail'][ count($userinfo['mail']) - 1 ] : $userinfo['mail'];
+
+ // smtp_mail($from, $to, $subject, $body, $headers = '')
+ $reset_link = sprintf('http://%s/lostpw/%s/%08x/%s', gethostname(), $userinfo['uid'], time(), hash_hmac('sha1', "{$userinfo['uid']}%" . time(), $hmac_secret));
+ $email_body = parse_template('emails/lostpw', array(
+ 'userinfo' => $userinfo
+ , 'reset_link' => $reset_link
+ , 'ip' => $_SERVER['REMOTE_ADDR']
+ ));
+
+ $domainname = strtolower(get_default_kerberos_realm());
+ $mail_result = smtp_mail("accounts@$domainname", $mail, "$domainname password reset", $email_body, "From: $domainname accounts <accounts@$domainname>\r\nTo: {$userinfo['cn']} <$mail>");
+
+ // censor e-mail address to keep deviants from scraping for it
+ if ( $field == 'uid' && preg_match('/^(.)(.*?)(.)@([a-z\.]+)$/', $mail, $match) )
+ $mail_censored = sprintf('%s%s%s@%s', $match[1], str_repeat('.', strlen($match[2])), $match[3], $match[4]);
+
+ if ( $mail_result )
+ {
+ queue_message(E_NOTICE, "Password reset instructions have been mailed to $mail_censored.");
+ }
+ else
+ {
+ throw new Exception("Failed to send e-mail.");
+ }
+ }
+ catch ( Exception $e )
+ {
+ queue_message(E_ERROR, $e->getMessage());
+ }
+}
+
+display_template('lostpw');
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/.htaccess Fri Jan 11 00:32:54 2013 -0500
@@ -0,0 +1,3 @@
+AuthType None
+Satisfy Any
+Require valid-user
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/ssoinabox.css Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/ssoinabox.css Fri Jan 11 00:32:54 2013 -0500
@@ -25,7 +25,7 @@
div.navbar a.brand span.siab-logo {
font-family: TransportDOTMedium, arial;
- text-transform: lowercase;
+ text-transform: uppercase;
letter-spacing: -1px;
position: relative;
top: 2px;
--- a/packages/ssoinabox-webui/root/usr/local/share/weblogin/ssoinabox/templates/login.tmpl Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/weblogin/ssoinabox/templates/login.tmpl Fri Jan 11 00:32:54 2013 -0500
@@ -99,7 +99,7 @@
<br />
<p class="links">
<a href="/lostpw">Forgot password?</a> •
- <a href="/security.html">Account security</a> •
+ <a href="/">Account Control Panel</a> •
<a href="/help.html">Help</a>
</p>
</form>
--- a/packages/ssoinabox-webui/root/usr/local/share/weblogin/ssoinabox/templates/logout.tmpl Tue Jan 08 23:21:25 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/weblogin/ssoinabox/templates/logout.tmpl Fri Jan 11 00:32:54 2013 -0500
@@ -33,7 +33,7 @@
[% END %]
<p class="links">
- <a href="/security.html">Account security</a> •
+ <a href="/">Account Control Panel</a> •
<a href="/help.html">Help</a>
</p>
</div>
--- a/resources/functions Tue Jan 08 23:21:25 2013 -0500
+++ b/resources/functions Fri Jan 11 00:32:54 2013 -0500
@@ -171,6 +171,7 @@
objectClass: posixGroup
description: Default POSIX group for users
gidNumber: 500
+memberUid: $username
dn: cn=rtp,ou=Groups,$ldap_suffix
cn: rtp
@@ -352,6 +353,8 @@
PHONE_EXT_MIN: 500
+hmac_secret: `generate_password 40`
+
EOF
chown root:www-data /usr/local/etc/ssoinabox/webcreds.yml