Add webathena authentication button for Zephyr users.

This shows up when you're not running a Zephyr mirroring bot and lets
you use Webathena to have us run it.  Obviously needs more docs.

Current problems include:

* supervisorctl reload ends up recreating /var/run/supervisor.sock
  with the wrong permissions, so it only works once in a row before
  you need to chmod that.

* /etc/supervisor/conf.d needs to be humbug-writeable; this is a clear
  local root vulnerability

* This uses SSH and thus is kinda slow.

(imported from commit 7029979615ffd50b10f126ce2cf9a85a5eefd7a2)
This commit is contained in:
Tim Abbott 2013-08-23 14:49:06 -04:00
parent 98aa534d3d
commit 134da30fdf
9 changed files with 317 additions and 7 deletions

35
bots/process_ccache Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/python
import sys
import os
import subprocess
import base64
short_user = sys.argv[1]
api_key = sys.argv[2]
ccache_data_encoded = sys.argv[3]
# Update the Kerberos ticket cache file
program_name = "zmirror-%s" % (short_user,)
with file("/home/humbug/ccache/%s" % (program_name,), "w") as f:
f.write(base64.b64decode(ccache_data_encoded))
# Setup API key
api_key_path = "/home/humbug/api-keys/%s" % (program_name,)
file(api_key_path, "w").write(api_key + "\n")
# Setup supervisord configuration
supervisor_path = "/etc/supervisor/conf.d/%s.conf" % (program_name,)
template = "/home/humbug/humbug/bots/zmirror_private.conf.template"
template_data = file(template).read()
session_path = "/home/humbug/zephyr_sessions/%s" % (program_name,)
file(supervisor_path, "w").write(template_data.replace("USERNAME", short_user))
# Delete your session
subprocess.check_call(["rm", "-f", session_path])
# Update your supervisor config, which may restart your mirror
subprocess.check_call(["supervisorctl", "reread"])
subprocess.check_call(["supervisorctl", "update"])
# Restart your mirror, in case it wasn't restarted by the previous
# (Otherwise if the mirror lost subs, this would do nothing)
# TODO: check whether we JUST restarted it first
subprocess.check_call(["supervisorctl", "restart", program_name])

View File

@ -0,0 +1,11 @@
[program:zmirror-USERNAME]
command=python /home/humbug/humbug/bots/zephyr_mirror_backend.py --root-path=/home/humbug/humbug/bots --user=USERNAME --enable-log=/home/humbug/logs/mirror-log-%(program_name)s --use-sessions --session-path=/home/humbug/zephyr_sessions/%(program_name)s --api-key-file=/home/humbug/api-keys/%(program_name)s
priority=200 ; the relative start priority (default 999)
autostart=true ; start at supervisord start (default: true)
autorestart=true ; whether/when to restart (default: unexpected)
stopsignal=TERM ; signal used to kill process (default TERM)
stopwaitsecs=30 ; max num secs to wait b4 SIGKILL (default 10)
user=humbug ; setuid to this UNIX account to run the program
redirect_stderr=true ; redirect proc stderr to stdout (default false)
stdout_logfile=/var/log/humbug/%(program_name)s.log ; stdout log path, NONE for none; default AUTO
environment=HOME="/home/humbug",USER="humbug",KRB5CCNAME="/home/humbug/ccache/%(program_name)s"

View File

@ -1394,6 +1394,45 @@ $(function () {
popovers.hide_all();
});
// Webathena integration code
$('#right-sidebar').on('click', '.webathena_login', function (e) {
$("#zephyr-mirror-error").hide();
var principal = ["zephyr", "zephyr"];
WinChan.open({
url: "https://webathena.mit.edu/#!request_ticket_v1",
relay_url: "https://webathena.mit.edu/relay.html",
params: {
realm: "ATHENA.MIT.EDU",
principal: principal
}
}, function (err, r) {
if (err) {
blueslip.warn(err);
return;
}
if (r.status !== "OK") {
blueslip.warn(r);
return;
}
$.ajax({
type: 'POST',
url: "/accounts/webathena_kerberos_login/",
data: {cred: JSON.stringify(r.session)},
dataType: 'json',
success: function (data, success) {
$("#zephyr-mirror-error").hide();
},
error: function (data, success) {
$("#zephyr-mirror-error").show();
}
});
});
e.preventDefault();
e.stopPropagation();
});
// End Webathena code
$(document).on('click', function (e) {
// Dismiss popovers if the user has clicked outside them
if ($('.popover-inner').has(e.target).length === 0) {

View File

@ -8,12 +8,18 @@
<br /><br /> Retrying soon... <br /><br />
</div>
<div class="alert alert_sidebar alert-error home-error-bar" id="zephyr-mirror-error">
<strong>Messages you send are not being mirrored to MIT
zephyr</strong> &mdash; Please check
that <a href="/zephyr#mirror">you are running the Zephyr mirror
script</a>. Once you've corrected this
issue, you can <a class="restart_get_updates_button">click
here</a> to update this alert.
<strong>Your Zephyr mirroring is not working.</strong>.
There are two ways to fix this:
<ul>
<li><a class="webathena_login">Grant Zulip permission to mirror
the messages for you</a>. (Recommended)
</li>
<li><a href="/zephyr#mirror" target="_blank">Run the
Zephyr mirroring script yourself</a>
</li>
</ul>
</div>
<div class="alert alert_sidebar alert-error home-error-bar" id="home-error"></div>
<div class="alert alert_sidebar alert-error home-error-bar" id="reloading-application"></div>

View File

@ -27,7 +27,7 @@ var globals =
+ ' invite ui util activity timerender MessageList MessageListView blueslip unread stream_list'
+ ' onboarding message_edit tab_bar emoji popovers navigate message_tour'
+ ' avatar feature_flags search_suggestion referral stream_color Dict'
+ ' Filter summary admin stream_data muting'
+ ' Filter summary admin stream_data muting WinChan'
// colorspace.js
+ ' colorspace'

185
zerver/lib/ccache.py Normal file
View File

@ -0,0 +1,185 @@
#!/usr/bin/python
# This file is adapted from samples/shellinabox/ssh-krb-wrapper in
# https://github.com/davidben/webathena, which has the following
# license:
#
# Copyright (c) 2013 David Benjamin and Alan Huang
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import base64
import json
import os
import readline
import struct
import subprocess
import sys
# Some DER encoding stuff. Bleh. This is because the ccache contains a
# DER-encoded krb5 Ticket structure, whereas Webathena deserializes
# into the various fields. Re-encoding in the client would be easy as
# there is already an ASN.1 implementation, but in the interest of
# limiting MIT Kerberos's exposure to malformed ccaches, encode it
# ourselves. To that end, here's the laziest DER encoder ever.
def der_encode_length(l):
if l <= 127:
return chr(l)
out = ""
while l > 0:
out = chr(l & 0xff) + out
l >>= 8
out = chr(len(out) | 0x80) + out
return out
def der_encode_tlv(tag, value):
return chr(tag) + der_encode_length(len(value)) + value
def der_encode_integer_value(val):
if not isinstance(val, (int, long)):
raise TypeError("int")
# base 256, MSB first, two's complement, minimum number of octets
# necessary. This has a number of annoying edge cases:
# * 0 and -1 are 0x00 and 0xFF, not the empty string.
# * 255 is 0x00 0xFF, not 0xFF
# * -256 is 0xFF 0x00, not 0x00
# Special-case to avoid an empty encoding.
if val == 0:
return "\x00"
sign = 0 # What you would get if you sign-extended the current high bit.
out = ""
# We can stop once sign-extension matches the remaining value.
while val != sign:
byte = val & 0xff
out = chr(byte) + out
sign = -1 if byte & 0x80 == 0x80 else 0
val >>= 8
return out
def der_encode_integer(val):
return der_encode_tlv(0x02, der_encode_integer_value(val))
def der_encode_int32(val):
if val < -2147483648 or val > 2147483647:
raise ValueError("Bad value")
return der_encode_integer(val)
def der_encode_uint32(val):
if val < 0 or val > 4294967295:
raise ValueError("Bad value")
return der_encode_integer(val)
def der_encode_string(val):
if not isinstance(val, unicode):
raise TypeError("unicode")
return der_encode_tlv(0x1b, val.encode("utf-8"))
def der_encode_octet_string(val):
if not isinstance(val, str):
raise TypeError("str")
return der_encode_tlv(0x04, val)
def der_encode_sequence(tlvs, tagged=True):
body = []
for i, tlv in enumerate(tlvs):
# Missing optional elements represented as None.
if not tlv:
continue
if tagged:
# Assume kerberos-style explicit tagging of components.
tlv = der_encode_tlv(0xa0 | i, tlv)
body.append(tlv)
return der_encode_tlv(0x30, "".join(body))
def der_encode_ticket(tkt):
return der_encode_tlv(
0x61, # Ticket
der_encode_sequence(
[der_encode_integer(5), # tktVno
der_encode_string(tkt["realm"]),
der_encode_sequence( # PrincipalName
[der_encode_int32(tkt["sname"]["nameType"]),
der_encode_sequence([der_encode_string(c)
for c in tkt["sname"]["nameString"]],
tagged=False)]),
der_encode_sequence( # EncryptedData
[der_encode_int32(tkt["encPart"]["etype"]),
(der_encode_uint32(tkt["encPart"]["kvno"])
if "kvno" in tkt["encPart"]
else None),
der_encode_octet_string(
base64.b64decode(tkt["encPart"]["cipher"]))])]))
# Kerberos ccache writing code. Using format documentation from here:
# http://www.gnu.org/software/shishi/manual/html_node/The-Credential-Cache-Binary-File-Format.html
def ccache_counted_octet_string(data):
if not isinstance(data, str):
raise TypeError("str")
return struct.pack("!I", len(data)) + data
def ccache_principal(name, realm):
header = struct.pack("!II", name["nameType"], len(name["nameString"]))
return (header + ccache_counted_octet_string(realm.encode("utf-8")) +
"".join(ccache_counted_octet_string(c.encode("utf-8"))
for c in name["nameString"]))
def ccache_key(key):
return (struct.pack("!H", key["keytype"]) +
ccache_counted_octet_string(base64.b64decode(key["keyvalue"])))
def flags_to_uint32(flags):
ret = 0
for i, v in enumerate(flags):
if v:
ret |= 1 << (31 - i)
return ret
def ccache_credential(cred):
out = ccache_principal(cred["cname"], cred["crealm"])
out += ccache_principal(cred["sname"], cred["srealm"])
out += ccache_key(cred["key"])
out += struct.pack("!IIII",
cred["authtime"] // 1000,
cred.get("starttime", cred["authtime"]) // 1000,
cred["endtime"] // 1000,
cred.get("renewTill", 0) // 1000)
out += struct.pack("!B", 0)
out += struct.pack("!I", flags_to_uint32(cred["flags"]))
# TODO: Care about addrs or authdata? Former is "caddr" key.
out += struct.pack("!II", 0, 0)
out += ccache_counted_octet_string(der_encode_ticket(cred["ticket"]))
# No second_ticket.
out += ccache_counted_octet_string("")
return out
def make_ccache(cred):
# Do we need a DeltaTime header? The ccache I get just puts zero
# in there, so do the same.
out = struct.pack("!HHHHII",
0x0504, # file_format_version
12, # headerlen
1, # tag (DeltaTime)
8, # taglen (two uint32_ts)
0, 0, # time_offset / usec_offset
)
out += ccache_principal(cred["cname"], cred["crealm"])
out += ccache_credential(cred)
return out

View File

@ -380,6 +380,38 @@ def accounts_accept_terms(request):
{ 'form': form, 'company_name': domain, 'email': email },
context_instance=RequestContext(request))
from zerver.lib.ccache import make_ccache
@authenticated_json_view
@has_request_variables
def webathena_kerberos_login(request, user_profile,
cred=REQ(default=None)):
if cred is None:
return json_error("Could not find Kerberos credential")
if not user_profile.realm.domain == "mit.edu":
return json_error("Webathena login only for mit.edu realm")
try:
parsed_cred = ujson.loads(cred)
user = parsed_cred["cname"]["nameString"][0]
assert(user == user_profile.email.split("@")[0])
ccache = make_ccache(parsed_cred)
except Exception:
return json_error("Invalid Kerberos cache")
# TODO: Send these data via (say) rabbitmq
try:
subprocess.check_call(["ssh", "humbug@zmirror2.zulip.net", "--",
"/home/humbug/humbug/bots/process_ccache",
user,
user_profile.api_key,
base64.b64encode(ccache)])
except Exception:
logging.exception("Error updating the user's ccache")
return json_error("We were unable to setup mirroring for you")
return json_success()
def api_endpoint_docs(request):
raw_calls = open('templates/zerver/api_content.json', 'r').read()
calls = ujson.loads(raw_calls)

View File

@ -358,6 +358,7 @@ JS_SPECS = {
'third/jquery-autosize/jquery.autosize.js',
'third/lazyload/lazyload.js',
'third/spectrum/spectrum.js',
'third/winchan/winchan.js',
('third/handlebars/handlebars.runtime.js'
if PIPELINE
else 'third/handlebars/handlebars.js'),

View File

@ -23,6 +23,7 @@ urlpatterns = patterns('',
url(r'^accounts/login/', 'zerver.views.login_page', {'template_name': 'zerver/login.html'}),
url(r'^accounts/login/', 'django.contrib.auth.views.login', {'template_name': 'zerver/login.html'}),
url(r'^accounts/logout/', 'zerver.views.logout_then_login'),
url(r'^accounts/webathena_kerberos_login/', 'zerver.views.webathena_kerberos_login'),
url(r'^accounts/password/reset/$', 'django.contrib.auth.views.password_reset',
{'post_reset_redirect' : '/accounts/password/reset/done/',