Repository :
http://git.fedorahosted.org/cgit/copr.git
On branch : master
---------------------------------------------------------------
commit 8d1c8135bcb261b394efb19460fbee45f2c31191
Author: Pavel Raiskup <praiskup(a)redhat.com>
Date: Thu Aug 21 11:35:38 2014 +0200
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(-)
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(a)REDHAT.COM is expected to have
+# user(a)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(a)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 1d520fe..9ec9985 100644
--- a/frontend/coprs_frontend/coprs/templates/layout.html
+++ b/frontend/coprs_frontend/coprs/templates/layout.html
@@ -23,7 +23,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__)
+(a)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())