Skip to content

Commit

Permalink
Add v3 API
Browse files Browse the repository at this point in the history
JIRA: RHELWF-7165
  • Loading branch information
hluk committed Nov 24, 2022
1 parent 5cb7df4 commit 920578b
Show file tree
Hide file tree
Showing 17 changed files with 832 additions and 714 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/resultsdb.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install system dependencies
uses: nick-invision/retry@v2
with:
timeout_minutes: 10
retry_wait_seconds: 30
max_attempts: 3
command: >-
sudo apt-get update
&& sudo apt-get install
libkrb5-dev
libldap2-dev
libsasl2-dev
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ RUN set -exo pipefail \
&& yum install -y \
--setopt install_weak_deps=false \
--nodocs \
gcc \
krb5-devel \
openldap-devel \
python39 \
python39-devel \
# install runtime dependencies
&& yum install -y \
--installroot=/mnt/rootfs \
--releasever=8 \
--setopt install_weak_deps=false \
--nodocs \
httpd \
krb5-libs \
mod_ssl \
openldap \
python39 \
python39-mod_wsgi \
&& yum --installroot=/mnt/rootfs clean all \
Expand Down
3 changes: 0 additions & 3 deletions conf/settings.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,8 @@ FEDMENU_DATA_URL = 'https://apps.fedoraproject.org/js/data.js'
AUTH_MODULE = None

# OIDC Configuration
OIDC_ADMINS = []
import os
OIDC_CLIENT_SECRETS = os.getcwd() + '/conf/oauth2_client_secrets.json'
OIDC_AUD = 'My-Client-ID'
OIDC_SCOPE = 'https://pagure.io/taskotron/resultsdb/access'
OIDC_RESOURCE_SERVER_ONLY = True


Expand Down
802 changes: 185 additions & 617 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ 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"

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

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

from resultsdb import proxy
from . import config

import flask
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

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


# the version as used in setup.py
__version__ = "2.2.0"
Expand Down Expand Up @@ -82,7 +82,7 @@ def jsonify_with_jsonp(*args, **kwargs):
default_config_file = os.getcwd() + '/conf/settings.py'
elif os.getenv('TEST') == 'true' or openshift == "0":
default_config_obj = 'resultsdb.config.TestingConfig'
default_config_file = os.getcwd() + '/conf/settings.py'
default_config_file = ''
else:
default_config_obj = 'resultsdb.config.ProductionConfig'
default_config_file = '/etc/resultsdb/settings.py'
Expand Down Expand Up @@ -149,66 +149,41 @@ def setup_logging():
root_logger.addHandler(file_handler)
app.logger.addHandler(file_handler)


setup_logging()

if app.config['SHOW_DB_URI']:
app.logger.debug('using DBURI: %s' % app.config['SQLALCHEMY_DATABASE_URI'])


# database
db = SQLAlchemy(app)

# Register auth
if app.config['AUTH_MODULE'] == 'oidc':
from flask_oidc import OpenIDConnect
oidc = OpenIDConnect(app)

def _check():
if flask.request.method == 'POST':
# We don't need to do auth for any non-POST
# Prefer POSTed access token: they don't get into the httpd logs
token = flask.request.form.get('_auth_token')
if token is None:
token = flask.request.args.get('_auth_token')
if token is None:
token = flask.request.json.get('_auth_token')
if not token:
app.logger.error('No token submitted')
return False
validity = oidc.validate_token(token, [app.config['OIDC_SCOPE']])
if validity is not True:
app.logger.error('Token validation error: %s', validity)
return False
try:
token_info = oidc._get_token_info(token)
except Exception as ex:
app.logger.error('get_token failed: %s' % ex)
return False
if token_info.get('sub') not in app.config['OIDC_ADMINS']:
app.logger.error('Subject %s is not admin' %
token_info.get('sub'))
return False
return True
elif flask.request.method == 'GET':
return True

def check_token():
result = _check()
if result is None:
return flask.jsonify({'error': 'server_error'})
elif result is False:
return flask.jsonify({'error': 'invalid_token',
'error_description': 'Invalid or no token'})
# If the check passed, we fall through. This returns None, telling
# Flask that it can proceed further with the request

app.before_request(check_token)

# register blueprints
is_authentication_enabled = app.config['AUTH_MODULE'] == 'oidc'

from resultsdb.controllers.main import main
app.register_blueprint(main)

from resultsdb.controllers.api_v2 import api as api_v2
app.register_blueprint(api_v2, url_prefix="/api/v2.0")

from resultsdb.controllers.api_v3 import api as api_v3, oidc
if is_authentication_enabled:
app.logger.info('OpenIDConnect authentication is enabled')
oidc.init_app(app)
app.oidc = oidc
else:
app.logger.info('OpenIDConnect authentication is disabled')
app.register_blueprint(api_v3, url_prefix="/api/v3")


@oidc.require_login
def login():
return {
'username': oidc.user_getfield('username'),
'email': oidc.user_getfield('email'),
'token': oidc.get_access_token(),
}


app.add_url_rule('/auth/oidclogin', view_func=login)

app.logger.debug("Finished ResultsDB initialization")
90 changes: 90 additions & 0 deletions resultsdb/authorization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-License-Identifier: GPL-2.0+
import logging
import re
from fnmatch import fnmatch

from werkzeug.exceptions import (
BadGateway,
InternalServerError,
Unauthorized,
)

log = logging.getLogger(__name__)


def get_group_membership(ldap, user, con, ldap_search):
try:
results = con.search_s(
ldap_search["BASE"],
ldap.SCOPE_SUBTREE,
ldap_search.get("SEARCH_STRING", "(memberUid={user})").format(user=user),
["cn"],
)
return [group[1]["cn"][0].decode("utf-8") for group in results]
except KeyError:
log.exception("LDAP_SEARCHES parameter should contain the BASE key")
raise InternalServerError("LDAP_SEARCHES parameter should contain the BASE key")
except ldap.SERVER_DOWN:
log.exception("The LDAP server is not reachable.")
raise BadGateway("The LDAP server is not reachable.")
except ldap.LDAPError:
log.exception("Some error occurred initializing the LDAP connection.")
raise Unauthorized("Some error occurred initializing the LDAP connection.")


def match_testcase_permissions(testcase, permissions):
for permission in permissions:
if "testcases" in permission:
testcase_match = any(
fnmatch(testcase, testcase_pattern)
for testcase_pattern in permission["testcases"]
)
elif "_testcase_regex_pattern" in permission:
testcase_match = re.search(permission["_testcase_regex_pattern"], testcase)
else:
continue

if testcase_match:
yield permission


def verify_authorization(user, testcase, permissions, ldap_host, ldap_searches):
if not (ldap_host and ldap_searches):
raise InternalServerError(
(
"LDAP_HOST and LDAP_SEARCHES also need to be defined "
"if PERMISSIONS is defined."
)
)

allowed_groups = []
for permission in match_testcase_permissions(testcase, permissions):
if user in permission.get("users", []):
return True
allowed_groups += permission.get("groups", [])

try:
import ldap
except ImportError:
raise InternalServerError(
("If PERMISSIONS is defined, " "python-ldap needs to be installed.")
)

try:
con = ldap.initialize(ldap_host)
except ldap.LDAPError:
log.exception("Some error occurred initializing the LDAP connection.")
raise Unauthorized("Some error occurred initializing the LDAP connection.")
group_membership = set()

for cur_ldap_search in ldap_searches:
group_membership.update(get_group_membership(ldap, user, con, cur_ldap_search))
if group_membership & set(allowed_groups):
return True

if not group_membership:
raise Unauthorized(f"Couldn't find user {user} in LDAP")

raise Unauthorized(
("You are not authorized to submit a result " f"for the test case {testcase}")
)
16 changes: 11 additions & 5 deletions resultsdb/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,13 @@ class Config(object):
# Extend the list of allowed outcomes.
ADDITIONAL_RESULT_OUTCOMES = ()

PERMISSIONS = []

# Supported values: "oidc"
AUTH_MODULE = None

# OIDC Configuration
OIDC_ADMINS = [] # should contain list of usernames that can do POSTs e.g. ['tflink', 'kparal']
OIDC_CLIENT_SECRETS = '/etc/resultsdb/oauth2_client_secrets.json'
OIDC_AUD = 'My-Client-ID'
OIDC_SCOPE = 'https://pagure.io/taskotron/resultsdb/access'
OIDC_REQUIRED_SCOPE = 'resultsdb_scope'
OIDC_RESOURCE_SERVER_ONLY = True

FEDMENU_URL = 'https://apps.fedoraproject.org/fedmenu'
Expand Down Expand Up @@ -130,13 +129,20 @@ class DevelopmentConfig(Config):
OIDC_CLIENT_SECRETS = os.getcwd() + '/conf/oauth2_client_secrets.json.example'


class TestingConfig(Config):
class TestingConfig(DevelopmentConfig):
TRAP_BAD_REQUEST_ERRORS = True
FEDMENU_URL = 'https://apps.stg.fedoraproject.org/fedmenu'
FEDMENU_DATA_URL = 'https://apps.stg.fedoraproject.org/js/data.js'
ADDITIONAL_RESULT_OUTCOMES = ('AMAZING',)
MESSAGE_BUS_PLUGIN = 'dummy'
MESSAGE_BUS_KWARGS = {}
PERMISSIONS = []
AUTH_MODULE = 'oidc'
LDAP_HOST = 'ldap://ldap.example.com'
LDAP_SEARCHES = [{
'BASE': 'ou=Groups,dc=example,dc=com',
'SEARCH_STRING': '(memberUid={user})',
}]


def openshift_config(config_object, openshift_production):
Expand Down
Loading

0 comments on commit 920578b

Please sign in to comment.