Skip to content

Commit

Permalink
Use Flask-pyoidc (release-engineering#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
hluk committed Jan 5, 2023
1 parent 0a01291 commit bc61205
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 215 deletions.
2 changes: 1 addition & 1 deletion conf/oauth2_client_secrets.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"auth_uri": "https://iddev.fedorainfracloud.org/openidc/Authorization",
"token_uri": "https://iddev.fedorainfracloud.org/openidc/Token",
"userinfo_uri": "https://iddev.fedorainfracloud.org/openidc/UserInfo",
"redirect_uris": [],
"redirect_uris": ["https://resultsdb.example.com/oidc_callback"],
"issuer": "https://iddev.fedorainfracloud.org/openidc/",
"token_introspection_uri": "https://iddev.fedorainfracloud.org/openidc/TokenInfo"
}
Expand Down
403 changes: 239 additions & 164 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,16 @@ pytest-cov = {version = "^4.0.0", optional = true}
tox = {version = "^3.28.0", optional = true}
tox-docker = {version = "^3.1.0", optional = true}

flask-oidc = "^1.4.0"
Flask-SQLAlchemy = "^3.0.2"
SQLAlchemy = {version = "^1.4.39"}
psycopg2-binary = {version = "^2.9.3"}
alembic = "^1.8.1"
iso8601 = "^1.0.2"
Flask-Pydantic = "^0.11.0"

# https://github.com/puiterwijk/flask-oidc/issues/147
itsdangerous = {version = "==2.0.1", optional = true}

email-validator = "^1.3.0"
python-ldap = "^3.4.3"
Flask-pyoidc = "^3.11.0"

[tool.poetry.extras]
test = [
Expand Down
85 changes: 67 additions & 18 deletions resultsdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,28 @@
# Josef Skladanka <jskladan@redhat.com>
# Ralph Bean <rbean@redhat.com>

import json
import logging
import logging.handlers
import logging.config as logging_config
import os

from flask import Flask, jsonify, session
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import (
ClientMetadata,
ProviderConfiguration,
ProviderMetadata,
)
from flask_pyoidc.user_session import UserSession

from resultsdb.proxy import ReverseProxied
from resultsdb.controllers.main import main
from resultsdb.controllers.api_v2 import api as api_v2
from resultsdb.controllers.api_v3 import api as api_v3, oidc
from resultsdb.controllers.api_v3 import api as api_v3, create_endpoints
from resultsdb.models import db
from . import config

import flask


# the version as used in setup.py
__version__ = "2.2.0"
Expand All @@ -43,7 +51,7 @@


def create_app(config_obj=None):
app = flask.Flask(__name__)
app = Flask(__name__)
app.secret_key = "replace-me-with-something-random"

# make sure app behaves when behind a proxy
Expand Down Expand Up @@ -96,6 +104,13 @@ def create_app(config_obj=None):
db.init_app(app)

register_handlers(app)

if app.config["AUTH_MODULE"] == "oidc":
app.logger.info("OpenIDConnect authentication is enabled")
enable_oidc(app)
else:
app.logger.info("OpenIDConnect authentication is disabled")

register_blueprints(app)

app.logger.debug("Finished ResultsDB initialization")
Expand Down Expand Up @@ -156,30 +171,64 @@ def register_handlers(app):
# TODO: find out why error handler works for 404 but not for 400
@app.errorhandler(400)
def bad_request(error):
return flask.jsonify({"message": "Bad request"}), 400
return jsonify({"message": "Bad request"}), 400

@app.errorhandler(404)
def not_found(error):
return flask.jsonify({"message": "Not found"}), 404
return jsonify({"message": "Not found"}), 404


def register_blueprints(app):
app.register_blueprint(main)
app.register_blueprint(api_v2, url_prefix="/api/v2.0")
app.register_blueprint(api_v3, url_prefix="/api/v3")

if app.config["AUTH_MODULE"] == "oidc":

@app.route("/auth/oidclogin")
@oidc.require_login
def login():
return {
"username": oidc.user_getfield(app.config["OIDC_USERNAME_FIELD"]),
"token": oidc.get_access_token(),
def enable_oidc(app):
with open(app.config["OIDC_CLIENT_SECRETS"]) as client_secrets_file:
client_secrets = json.load(client_secrets_file)

provider = app.config.get("OIDC_PROVIDER", "web")
metadata = client_secrets[provider]
app.config.update(
{
"OIDC_PROVIDER": provider,
"OIDC_REDIRECT_URI": metadata["redirect_uris"][0],
}
)
client_metadata = ClientMetadata(metadata["client_id"], metadata["client_secret"])
provider_metadata = ProviderMetadata(
issuer=metadata["issuer"],
authorization_endpoint=metadata["auth_uri"],
token_endpoint=metadata["token_uri"],
userinfo_endpoint=metadata["userinfo_uri"],
introspection_endpoint=metadata["token_introspection_uri"],
jwks_uri=metadata.get("jwks_uri", metadata["token_uri"].replace("/token", "/certs")),
)
config = ProviderConfiguration(
issuer=metadata["issuer"],
client_metadata=client_metadata,
provider_metadata=provider_metadata,
session_refresh_interval_seconds=app.config["OIDC_SESSION_REFRESH_INTERVAL_SECONDS"],
)
oidc = OIDCAuthentication({provider: config}, app)

@app.route("/auth/oidclogin")
@oidc.oidc_auth(provider)
def login():
user_session = UserSession(session)
return jsonify(
{
"username": user_session.userinfo[app.config["OIDC_USERNAME_FIELD"]],
"token": user_session.access_token,
}
)

oidc.init_app(app)
app.oidc = oidc
app.logger.info("OpenIDConnect authentication is enabled")
else:
app.logger.info("OpenIDConnect authentication is disabled")
@app.route("/auth/logout")
@oidc.oidc_logout
def logout():
return jsonify({"message": "Logged out"})

app.oidc = oidc

create_endpoints(oidc, provider)
6 changes: 5 additions & 1 deletion resultsdb/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ class Config(object):
AUTH_MODULE = None

OIDC_CLIENT_SECRETS = "/etc/resultsdb/oauth2_client_secrets.json"
OIDC_REQUIRED_SCOPE = "resultsdb_scope"
OIDC_USERNAME_FIELD = "uid"
OIDC_SESSION_REFRESH_INTERVAL_SECONDS = 300
OIDC_SESSION_PERMANENT = False
PERMANENT_SESSION_LIFETIME = 300

FEDMENU_URL = "https://apps.fedoraproject.org/fedmenu"
FEDMENU_DATA_URL = "https://apps.fedoraproject.org/js/data.js"
Expand Down Expand Up @@ -149,6 +151,8 @@ class TestingConfig(DevelopmentConfig):
}
]

OIDC_CLIENT_SECRETS = os.getcwd() + "/conf/oauth2_client_secrets.json.example"


def openshift_config(config_object, openshift_production):
# First, get db details from env
Expand Down
17 changes: 6 additions & 11 deletions resultsdb/controllers/api_v3.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: GPL-2.0+
from flask import Blueprint, jsonify, g, render_template
from flask import Blueprint, jsonify, render_template
from flask import current_app as app
from flask_oidc import OpenIDConnect
from flask_pydantic import validate

from resultsdb.models import db
Expand All @@ -20,7 +19,6 @@
)

api = Blueprint("api_v3", __name__)
oidc = OpenIDConnect()


def permissions():
Expand All @@ -34,7 +32,7 @@ def _verify_authorization(user, testcase):


def create_result(body: ResultParamsBase):
user = g.oidc_token_info[app.config["OIDC_USERNAME_FIELD"]]
user = app.oidc.current_token_identity[app.config["OIDC_USERNAME_FIELD"]]
_verify_authorization(user, body.testcase)

testcase = Testcase.query.filter_by(name=body.testcase).first()
Expand Down Expand Up @@ -65,10 +63,10 @@ def create_result(body: ResultParamsBase):
return commit_result(result)


def create_endpoint(params_class):
def create_endpoint(params_class, oidc, provider):
params = params_class.construct()

@oidc.accept_token(require_token=True)
@oidc.token_auth(provider)
@validate()
def create(body: params_class):
return create_result(body)
Expand All @@ -90,9 +88,9 @@ def get_schema():
)


def create_endpoints():
def create_endpoints(oidc, provider):
for params_class in RESULTS_PARAMS_CLASSES:
create_endpoint(params_class)
create_endpoint(params_class, oidc, provider)


@api.route("/permissions")
Expand Down Expand Up @@ -134,6 +132,3 @@ def index():
endpoints=endpoints,
result_outcomes_extended=", ".join(result_outcomes_extended()),
)


create_endpoints()
13 changes: 12 additions & 1 deletion testing/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from unittest.mock import patch

import pytest

Expand All @@ -7,7 +8,17 @@


@pytest.fixture(scope="session", autouse=True)
def app():
def mock_oidc():
with patch("resultsdb.OIDCAuthentication") as oidc:
oidc().token_auth.side_effect = lambda _provider: lambda fn: fn
oidc().oidc_auth.side_effect = lambda _provider: lambda fn: fn
oidc().oidc_logout.side_effect = lambda _provider: lambda fn: fn
oidc().current_token_identity = {"uid": "testuser1"}
yield


@pytest.fixture(scope="session", autouse=True)
def app(mock_oidc):
app = create_app("resultsdb.config.TestingConfig")
with app.app_context():
db.drop_all()
Expand Down
24 changes: 9 additions & 15 deletions testing/test_api_v3.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# SPDX-License-Identifier: GPL-2.0+
from unittest.mock import ANY, patch, Mock

import flask
import pytest

from resultsdb.models import db
from resultsdb.parsers.api_v3 import RESULTS_PARAMS_CLASSES
from resultsdb.controllers.api_v3 import oidc


@pytest.fixture(scope="function", autouse=True)
def db_session():
db.session.rollback()
db.drop_all()
db.create_all()


@pytest.fixture(autouse=True)
Expand All @@ -17,18 +23,6 @@ def mock_ldap():
yield con


@pytest.fixture(autouse=True)
def mock_oidc():
with patch.object(oidc, "validate_token") as validate:

def validate_side_effect(*args, **kwargs):
flask.g.oidc_token_info = {"uid": "testuser1"}
return True

validate.side_effect = validate_side_effect
yield


@pytest.fixture
def client(app):
return app.test_client()
Expand Down Expand Up @@ -133,7 +127,7 @@ def test_api_v3_create_redhat_container_image(client):
assert r.json["testcase"] == {
"href": "http://localhost/api/v2.0/testcases/testcase1",
"name": "testcase1",
"ref_url": "https://test.example.com/docs/testcase1",
"ref_url": None,
}
assert r.json["data"]["item"] == [data["item"]]
assert r.json["data"]["type"] == ["redhat-container-image"]
Expand Down

0 comments on commit bc61205

Please sign in to comment.