>From 622097d95d744c86bdf28cd779c9991da2f146ee Mon Sep 17 00:00:00 2001
From: Pavel Raiskup <praiskup@redhat.com>
Date: Thu, 21 Aug 2014 11:35:38 +0200
Subject: [PATCH 2/2] [frontend] support for kerberos authentication

The krb auth method is aimed to be self-standing auth method, but
also can be used as alternative way how to connect to Copr
in parallel with Fedora OpenID authentication method.

The OpenID is currently still the default and the Copr 'username'
is bound to OpenID username (such usernames must match);  this
should be fixed in future so that the Copr user is 1:N mapped to
OpenID credentials (similarly like the krb is).
---
 .../versions/2a4242380f24_add_krb5_login_table.py  |  27 ++++++
 frontend/coprs_frontend/config/copr.conf           |  24 +++++
 frontend/coprs_frontend/coprs/config.py            |   3 +
 frontend/coprs_frontend/coprs/models.py            |  19 ++++
 frontend/coprs_frontend/coprs/templates/403.html   |  10 ++
 .../coprs_frontend/coprs/templates/layout.html     |   7 +-
 frontend/coprs_frontend/coprs/views/misc.py        | 106 ++++++++++++++++++---
 7 files changed, 182 insertions(+), 14 deletions(-)
 create mode 100644 frontend/coprs_frontend/alembic/versions/2a4242380f24_add_krb5_login_table.py
 create mode 100644 frontend/coprs_frontend/coprs/templates/403.html

diff --git a/frontend/coprs_frontend/alembic/versions/2a4242380f24_add_krb5_login_table.py b/frontend/coprs_frontend/alembic/versions/2a4242380f24_add_krb5_login_table.py
new file mode 100644
index 0000000..08a7b0f
--- /dev/null
+++ b/frontend/coprs_frontend/alembic/versions/2a4242380f24_add_krb5_login_table.py
@@ -0,0 +1,27 @@
+"""Add 'krb5_login' table
+
+Revision ID: 2a4242380f24
+Revises: bd0a15c7b6f
+Create Date: 2014-08-21 11:40:19.181293
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2a4242380f24'
+down_revision = 'bd0a15c7b6f'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.create_table('krb5_login',
+        sa.Column('user_id', sa.Integer(), nullable=False),
+        sa.Column('config_name', sa.String(length=30), nullable=False),
+        sa.Column('primary', sa.String(length=80), nullable=False),
+        sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+        sa.PrimaryKeyConstraint('primary', 'config_name')
+    )
+
+def downgrade():
+    op.drop_table('krb5_login')
diff --git a/frontend/coprs_frontend/config/copr.conf b/frontend/coprs_frontend/config/copr.conf
index 2d39d3b..4f70fe7 100644
--- a/frontend/coprs_frontend/config/copr.conf
+++ b/frontend/coprs_frontend/config/copr.conf
@@ -37,3 +37,27 @@ SQLALCHEMY_ECHO = False
 
 # send emails when user's perms change in project?
 SEND_EMAILS = True
+
+# Kerberos authentication configuration.  Example configuration abbreviated
+# like 'RH' (should not changed once used, reflects what is stored in database).
+# This requires mod_auth_kerb package (Fedora/RHEL) installed on your frontend
+# machine.
+#
+# log_text        - What text will be shown in top-menu link pointing to URI
+#                   with particular Kerberos login
+# URI             - Users can do authentication on /krb5_login/<URI>/
+#                   (accessible from top-menu link).  Note that apache must be
+#                   configured to perforem KRB authentication on the
+#                   /krb5_login/<URI>/ URI.  See mod
+# email_domain    - For simplicity, we expect that all users coming from the
+#                   same krb realm have the same (implicit) email domain.  E.g.
+#                   kerberos user user@REDHAT.COM is expected to have
+#                   user@redhat.com email address.
+
+#KRB5_LOGIN = {
+#    'RH': {
+#        'URI':          "redhat",
+#        'log_text':     "Krb5 login",
+#        'email_domain': "redhat.com",
+#    }
+#}
diff --git a/frontend/coprs_frontend/coprs/config.py b/frontend/coprs_frontend/coprs/config.py
index 31b3618..14be052 100644
--- a/frontend/coprs_frontend/coprs/config.py
+++ b/frontend/coprs_frontend/coprs/config.py
@@ -10,6 +10,9 @@ class Config(object):
     SECRET_KEY = "THISISNOTASECRETATALL"
     BACKEND_PASSWORD = "thisisbackend"
 
+    KRB5_LOGIN_BASEURI = "/krb5_login/"
+    KRB5_LOGIN = {}
+
     # restrict access to a set of users
     USE_ALLOWED_USERS = False
     ALLOWED_USERS = []
diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py
index 420f49c..f2d8838 100644
--- a/frontend/coprs_frontend/coprs/models.py
+++ b/frontend/coprs_frontend/coprs/models.py
@@ -501,3 +501,22 @@ class Action(db.Model, helpers.Serializer):
 
         return "Action {0} on {1}, old value: {2}, new value: {3}.".format(
             self.action_type, self.object_type, self.old_value, self.new_value)
+
+
+class Krb5Login(db.Model, helpers.Serializer):
+    """
+    Represents additional user information for kerberos authentication.
+    """
+
+    __tablename__ = "krb5_login"
+
+    # FK to User table
+    user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+
+    # 'string' from 'copr.conf' from KRB5_LOGIN[string]
+    config_name = db.Column(db.String(30), nullable=False, primary_key=True)
+
+    # krb's primary, i.e. 'username' from 'username@EXAMPLE.COM'
+    primary = db.Column(db.String(80), nullable=False, primary_key=True)
+
+    user = db.relationship("User", backref=db.backref("krb5_logins"))
diff --git a/frontend/coprs_frontend/coprs/templates/403.html b/frontend/coprs_frontend/coprs/templates/403.html
new file mode 100644
index 0000000..ec60202
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/templates/403.html
@@ -0,0 +1,10 @@
+{% extends "layout.html" %}
+{% block title %}Permission denied{% endblock %}
+{% block header %}Permission denied!{% endblock %}
+{% block body %}
+  {% if message %}
+    {{ message }}
+  {% else %}
+    You are not allowed to see this page.
+  {% endif %}
+{% endblock %}
diff --git a/frontend/coprs_frontend/coprs/templates/layout.html b/frontend/coprs_frontend/coprs/templates/layout.html
index 66823e4..c7813e4 100644
--- a/frontend/coprs_frontend/coprs/templates/layout.html
+++ b/frontend/coprs_frontend/coprs/templates/layout.html
@@ -18,7 +18,12 @@
       {% if g.user %}
         <div><span class="text">logged in as </span><a href="https://admin.fedoraproject.org/accounts/user/view/{{ g.user.name }}">{{ g.user.name }}</a> | <a href="{{ url_for('misc.logout') }}">log out</a></div>
       {% else %}
-        <div><a href="{{ url_for('misc.login') }}">log in</a> | <a href="https://admin.fedoraproject.org/accounts/user/new">sign up</a></div>
+        <div>
+            <a href="{{ url_for('misc.login') }}">log in</a> |
+            {% for key,val in config.KRB5_LOGIN.iteritems() %}
+            <a href="{{ config.KRB5_LOGIN_BASEURI }}{{ val.URI }}/">{{ val.log_text }}</a> |
+            {% endfor %}
+            <a href="https://admin.fedoraproject.org/accounts/user/new">sign up</a></div>
       {% endif %}
       <form method="get" action="{{ url_for('coprs_ns.coprs_fulltext_search') }}">
         <input class="rounded" type="text" name="fulltext">
diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py
index 0d84df7..5650c4c 100644
--- a/frontend/coprs_frontend/coprs/views/misc.py
+++ b/frontend/coprs_frontend/coprs/views/misc.py
@@ -2,6 +2,7 @@ import base64
 import datetime
 import functools
 
+import re
 import flask
 
 from coprs import app
@@ -20,15 +21,39 @@ def fed_openidize_name(name):
 
     return "http://{0}.id.fedoraproject.org/".format(name)
 
+
+def create_user_wrapper(username, email, timezone=None):
+    expiration_date_token = datetime.date.today() + \
+        datetime.timedelta(
+            days=flask.current_app.config["API_TOKEN_EXPIRATION"])
+
+    copr64 = base64.b64encode("copr") + "##"
+    user = models.User(username=username, mail=email,
+                       timezone=timezone,
+                       api_login=copr64 + helpers.generate_api_token(
+                           app.config["API_TOKEN_LENGTH"] - len(copr64)),
+                       api_token=helpers.generate_api_token(
+                           app.config["API_TOKEN_LENGTH"]),
+                       api_token_expiration=expiration_date_token)
+    return user
+
+
 def fed_raw_name(oidname):
     return oidname.replace(".id.fedoraproject.org/", "") \
                   .replace("http://", "")
 
+def krb_strip_realm(fullname):
+    return re.sub(r'@.*', '', fullname)
+
 @app.before_request
 def lookup_current_user():
-    flask.g.user = None
+    flask.g.user = username = None
     if "openid" in flask.session:
         username = fed_raw_name(flask.session["openid"])
+    elif "krb5_login" in flask.session:
+        username = flask.session["krb5_login"]
+
+    if username:
         flask.g.user = models.User.query.filter(
             models.User.username == username).first()
 
@@ -41,6 +66,71 @@ def page_not_found(message):
 misc = flask.Blueprint("misc", __name__)
 
 
+@misc.route(app.config['KRB5_LOGIN_BASEURI'] + "<name>/", methods=["GET"])
+def krb5_login(name):
+    """
+    Handle the Kerberos authentication.
+
+    Note that if we are able to get here, either the user is authenticated
+    correctly, or apache is mis-configured and it does not perform KRB
+    authentication at all.  Note also, even if that can be considered ugly, we
+    are reusing oid's get_next_url feature with kerberos login.
+    """
+
+    # Already logged in?
+    if flask.g.user is not None:
+        return flask.redirect(oid.get_next_url())
+
+    krb_config = app.config['KRB5_LOGIN']
+
+    found = None
+    for key in krb_config.keys():
+        if krb_config[key]['URI'] == name:
+            found = key
+            break
+
+    if not found:
+        # no KRB5_LOGIN.<name> configured in copr.conf
+        return flask.render_template("404.html"), 404
+
+    if not 'REMOTE_USER' in flask.request.environ:
+        nocred = "Kerberos authentication failed (no credentials provided)"
+        return flask.render_template("403.html",
+                message=nocred), 403
+
+    krb_username = flask.request.environ['REMOTE_USER']
+    username = krb_strip_realm(krb_username)
+
+    login = models.Krb5Login.query \
+            .filter(models.Krb5Login.config_name == key) \
+            .filter(models.Krb5Login.primary == username) \
+            .first()
+    if login:
+        flask.g.user = login.user
+        flask.session['krb5_login'] = login.user.name
+        flask.flash(u"Welcome, {0}".format(flask.g.user.name))
+        return flask.redirect(oid.get_next_url())
+
+    # We need to create row in 'krb5_login' table
+    user = models.User.query \
+            .filter(models.User.username == username) \
+            .first()
+    if not user:
+        # Even the item in 'user' table does not exist, create _now_
+        email = username + "@" + krb_config[key]['email_domain']
+        user = create_user_wrapper(username, email)
+        db.session.add(user)
+
+    login = models.Krb5Login(user=user, primary=username, config_name=key)
+    db.session.add(login)
+    db.session.commit()
+
+    flask.flash(u"Welcome, {0}".format(user.name))
+    flask.g.user = user
+    flask.session['krb5_login'] = user.name
+    return flask.redirect(oid.get_next_url())
+
+
 @misc.route("/login/", methods=["GET"])
 @oid.loginhandler
 def login():
@@ -66,18 +156,7 @@ def create_or_login(resp):
         user = models.User.query.filter(
             models.User.username == username).first()
         if not user:  # create if not created already
-            expiration_date_token = datetime.date.today() + \
-                datetime.timedelta(
-                    days=flask.current_app.config["API_TOKEN_EXPIRATION"])
-
-            copr64 = base64.b64encode("copr") + "##"
-            user = models.User(username=username, mail=resp.email,
-                               timezone=resp.timezone,
-                               api_login=copr64 + helpers.generate_api_token(
-                                   app.config["API_TOKEN_LENGTH"] - len(copr64)),
-                               api_token=helpers.generate_api_token(
-                                   app.config["API_TOKEN_LENGTH"]),
-                               api_token_expiration=expiration_date_token)
+            user = create_user_wrapper(username, resp.email, resp.timezone)
         else:
             user.mail = resp.email
             user.timezone = resp.timezone
@@ -99,6 +178,7 @@ def create_or_login(resp):
 @misc.route("/logout/")
 def logout():
     flask.session.pop("openid", None)
+    flask.session.pop("krb5_login", None)
     flask.flash(u"You were signed out")
     return flask.redirect(oid.get_next_url())
 
-- 
1.9.3

