From 0b76faafaef2d77b9e9d38f63e81bb9f4d265bbc Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Apr 01 2022 10:08:31 +0000 Subject: frontend, python: put /gssapi_login/ page under apiv3 This fixes the broken name of the route, and also breaks some circular imports. While fixing circular deps, I had to move the create_user_wrapper to a misc & apiv3 neutral place (UsersLogic). --- diff --git a/frontend/coprs_frontend/commands/add_user.py b/frontend/coprs_frontend/commands/add_user.py index 92aaea3..912a658 100644 --- a/frontend/coprs_frontend/commands/add_user.py +++ b/frontend/coprs_frontend/commands/add_user.py @@ -1,7 +1,7 @@ import click from coprs import db, app from coprs import models -from coprs.views.misc import create_user_wrapper +from coprs.logic.users_logic import UsersLogic @click.command() @click.argument("name") @@ -28,7 +28,7 @@ def add_user_function(name, mail, api_token=None, api_login=None): print("User named {0} already exists.".format(name)) return - user = create_user_wrapper(name, mail) + user = UsersLogic.create_user_wrapper(name, mail) if api_token: user.api_token = api_token if api_login: diff --git a/frontend/coprs_frontend/coprs/context_processors.py b/frontend/coprs_frontend/coprs/context_processors.py index 301092c..c6f9354 100644 --- a/frontend/coprs_frontend/coprs/context_processors.py +++ b/frontend/coprs_frontend/coprs/context_processors.py @@ -59,7 +59,7 @@ def login_menu(): if config['KRB5_LOGIN']: menu.append({ - 'link': flask.url_for("misc.krb5_login"), + 'link': flask.url_for("apiv3_ns.gssapi_login"), 'desc': config['KRB5_LOGIN']['log_text'], }) diff --git a/frontend/coprs_frontend/coprs/logic/users_logic.py b/frontend/coprs_frontend/coprs/logic/users_logic.py index 6d33177..70f492e 100644 --- a/frontend/coprs_frontend/coprs/logic/users_logic.py +++ b/frontend/coprs_frontend/coprs/logic/users_logic.py @@ -1,11 +1,12 @@ +import base64 import json -from datetime import date +import datetime from coprs import exceptions from flask import url_for from coprs import app, db from coprs.models import User, Group -from coprs.helpers import copr_url +from coprs.helpers import copr_url, generate_api_token from sqlalchemy import update @@ -125,11 +126,31 @@ class UsersLogic(object): "admin": False, "api_login": "", "api_token": "", - "api_token_expiration": date(1970, 1, 1), + "api_token_expiration": datetime.date(1970, 1, 1), "openid_groups": None} for k, v in null.items(): setattr(user, k, v) + @classmethod + def create_user_wrapper(cls, username, email, timezone=None): + """ + Initial creation of Copr user (creates the API token, too). + Create user + token configuration. + """ + expiration_date_token = datetime.date.today() + \ + datetime.timedelta( + days=app.config["API_TOKEN_EXPIRATION"]) + + copr64 = base64.b64encode(b"copr") + b"##" + user = User(username=username, mail=email, + timezone=timezone, + api_login=copr64.decode("utf-8") + generate_api_token( + app.config["API_TOKEN_LENGTH"] - len(copr64)), + api_token=generate_api_token( + app.config["API_TOKEN_LENGTH"]), + api_token_expiration=expiration_date_token) + return user + class UserDataDumper(object): def __init__(self, user): diff --git a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py index d17e784..48420f4 100644 --- a/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py +++ b/frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_general.py @@ -1,11 +1,12 @@ +import os +import re + import flask + +from coprs import app, oid, db, models from coprs.views.apiv3_ns import apiv3_ns from coprs.views.misc import api_login_required - - -@apiv3_ns.route("/") -def home(): - return flask.jsonify({"version": 3}) +from coprs.logic.users_logic import UsersLogic def auth_check_response(): @@ -15,7 +16,104 @@ def auth_check_response(): return flask.g.user.to_dict() +def gssapi_login_action(): + """ + Redirect the successful log-in attempt, or return the JSON data that user + expects. + """ + if "web-ui" in flask.request.full_path: + return flask.redirect(oid.get_next_url()) + return flask.jsonify(auth_check_response()) + + +def krb_straighten_username(krb_remote_user): + """ + Cleanup the user's principal, and return just simple username. Remove + disallowed characters for the service principals. + """ + # Input should look like 'USERNAME@REALM.TLD', strip realm. + username = re.sub(r'@.*', '', krb_remote_user) + + # But USERNAME part can consist of USER/DOMAIN.TLD. + # TODO: Do we need more clever thing here? + username = re.sub('/', '_', username) + + # Based on restrictions for project name: "letters, digits, underscores, + # dashes and dots", it is worth limitting the username here, too. + # TODO: Store this pattern on one place. + return username if re.match(r"^[\w.-]+$", username) else None + + +@apiv3_ns.route("/") +def home(): + return flask.jsonify({"version": 3}) + + @apiv3_ns.route("/auth-check") @api_login_required def auth_check(): return flask.jsonify(auth_check_response()) + + +@apiv3_ns.route("/gssapi_login/", methods=["GET"]) +@apiv3_ns.route("/gssapi_login/web-ui/", methods=["GET"]) +def gssapi_login(): + """ + Log-in using the GSSAPI/Kerberos credentials + + 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 (REMOTE_USER wouldn't be set, see below). + """ + + # Already logged in? + if flask.g.user is not None: + return gssapi_login_action() + + krb_config = app.config['KRB5_LOGIN'] + + if app.config["DEBUG"] and 'TEST_REMOTE_USER' in os.environ: + # For local testing (without krb5 keytab and other configuration) + flask.request.environ['REMOTE_USER'] = os.environ['TEST_REMOTE_USER'] + + if 'REMOTE_USER' not 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'] + app.logger.debug("krb5 login attempt: " + krb_username) + username = krb_straighten_username(krb_username) + if not username: + message = "invalid krb5 username: " + krb_username + return flask.render_template("403.html", message=message), 403 + + krb_login = ( + models.Krb5Login.query + .filter(models.Krb5Login.primary == username) + .first() + ) + + # TODO: require the FAS login first! + + if krb_login: + flask.g.user = krb_login.user + flask.session['krb5_login'] = krb_login.user.name + flask.flash("Welcome, {0}".format(flask.g.user.name), "success") + return gssapi_login_action() + + # 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['email_domain'] + user = UsersLogic.create_user_wrapper(username, email) + db.session.add(user) + + krb_login = models.Krb5Login(user=user, primary=username) + db.session.add(krb_login) + db.session.commit() + + flask.flash("Welcome, {0}".format(user.name), "success") + flask.g.user = user + flask.session['krb5_login'] = user.name + return gssapi_login_action() diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py index 84d4e9d..20897b4 100644 --- a/frontend/coprs_frontend/coprs/views/misc.py +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -1,9 +1,7 @@ -import os import base64 import datetime import functools from functools import wraps, partial -import re from urllib.parse import urlparse import flask @@ -21,23 +19,6 @@ from coprs.logic.complex_logic import ComplexLogic from coprs.logic.users_logic import UsersLogic from coprs.exceptions import ObjectNotFound from coprs.measure import checkpoint_start -#from coprs.views.apiv3_ns import apiv3_general - - -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(b"copr") + b"##" - user = models.User(username=username, mail=email, - timezone=timezone, - api_login=copr64.decode("utf-8") + 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): @@ -48,20 +29,6 @@ def fed_raw_name(oidname): return oidname_parse.netloc.replace(".{0}".format(config_parse.netloc), "") -def krb_straighten_username(krb_remote_user): - # Input should look like 'USERNAME@REALM.TLD', strip realm. - username = re.sub(r'@.*', '', krb_remote_user) - - # But USERNAME part can consist of USER/DOMAIN.TLD. - # TODO: Do we need more clever thing here? - username = re.sub('/', '_', username) - - # Based on restrictions for project name: "letters, digits, underscores, - # dashes and dots", it is worth limitting the username here, too. - # TODO: Store this pattern on one place. - return username if re.match(r"^[\w.-]+$", username) else None - - @app.before_request def before_request(): """ @@ -111,77 +78,6 @@ conflict_request_handler = partial(generic_error, code=409, title="Conflict") misc = flask.Blueprint("misc", __name__) -def gssapi_login_action(): - """ - Redirect the successful log-in attempt, or return the JSON data that user - expects. - """ - if "web-ui" in flask.request.full_path: - return flask.redirect(oid.get_next_url()) - return flask.jsonify(apiv3_general.auth_check_response()) - - -@misc.route("/api_v3/gssapi_login/", methods=["GET"]) -@misc.route("/api_v3/gssapi_login/web-ui/", methods=["GET"]) -def krb5_login(): - """ - Log-in using the GSSAPI/Kerberos credentials - - 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 (REMOTE_USER wouldn't be set, see below). - """ - - # Already logged in? - if flask.g.user is not None: - return gssapi_login_action() - - krb_config = app.config['KRB5_LOGIN'] - - if app.config["DEBUG"] and 'TEST_REMOTE_USER' in os.environ: - # For local testing (without krb5 keytab and other configuration) - flask.request.environ['REMOTE_USER'] = os.environ['TEST_REMOTE_USER'] - - if 'REMOTE_USER' not 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'] - app.logger.debug("krb5 login attempt: " + krb_username) - username = krb_straighten_username(krb_username) - if not username: - message = "invalid krb5 username: " + krb_username - return flask.render_template("403.html", message=message), 403 - - krb_login = ( - models.Krb5Login.query - .filter(models.Krb5Login.primary == username) - .first() - ) - if krb_login: - flask.g.user = krb_login.user - flask.session['krb5_login'] = krb_login.user.name - flask.flash(u"Welcome, {0}".format(flask.g.user.name), "success") - return gssapi_login_action() - - # 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['email_domain'] - user = create_user_wrapper(username, email) - db.session.add(user) - - krb_login = models.Krb5Login(user=user, primary=username) - db.session.add(krb_login) - db.session.commit() - - flask.flash(u"Welcome, {0}".format(user.name), "success") - flask.g.user = user - flask.session['krb5_login'] = user.name - return gssapi_login_action() - - def workaround_ipsilon_email_login_bug_handler(f): """ We are working around an ipislon issue when people log in with their email, @@ -253,7 +149,8 @@ def create_or_login(resp): user = models.User.query.filter( models.User.username == username).first() if not user: # create if not created already - user = create_user_wrapper(username, resp.email, resp.timezone) + user = UsersLogic.create_user_wrapper(username, resp.email, + resp.timezone) else: user.mail = resp.email user.timezone = resp.timezone @@ -322,7 +219,7 @@ def api_login_required(f): def krb5_login_redirect(next=None): if app.config['KRB5_LOGIN']: # Pick the first one for now. - return flask.redirect(flask.url_for("misc.krb5_login", + return flask.redirect(flask.url_for("apiv3_ns.krb5_login", next=next)) flask.flash("Unable to pick krb5 login page", "error") return flask.redirect(flask.url_for("coprs_ns.coprs_show")) diff --git a/python/copr/v3/helpers.py b/python/copr/v3/helpers.py index 74ff442..73e09b7 100644 --- a/python/copr/v3/helpers.py +++ b/python/copr/v3/helpers.py @@ -146,7 +146,7 @@ def get_session_cookie(config): :return: Munch """ - url = config["copr_url"] + "/api_v3/gssapi_login/" + url = config["copr_url"] + "/api_3/gssapi_login/" session = requests.Session() response = None try: