From 8cb4e0de5d20140b65837fe67455405164afbcfc Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Fri, 2 Dec 2016 14:39:14 +0100 Subject: [PATCH] Use OIDC to auth the users, replace submit-build.sh by submit-build.py which does hackish way of OIDC just to test things. --- conf/config.py | 2 + manage.py | 2 - module_build_service/auth.py | 79 ++++++++++++++++++++++---------- module_build_service/views.py | 5 +- submit-build.sh | 11 ----- submit_build.py | 84 ++++++++++++++++++++++++++++++++++ tests/test_auth.py | 31 +++++++++++-- tests/test_views/test_views.py | 4 +- 8 files changed, 172 insertions(+), 46 deletions(-) delete mode 100755 submit-build.sh create mode 100644 submit_build.py diff --git a/conf/config.py b/conf/config.py index 905d0013..e8682c0d 100644 --- a/conf/config.py +++ b/conf/config.py @@ -116,6 +116,8 @@ class DevConfiguration(BaseConfiguration): KOJI_ARCHES = ['x86_64'] KOJI_REPOSITORY_URL = 'http://kojipkgs.stg.fedoraproject.org/repos' + OIDC_CLIENT_SECRETS = "client_secrets.json" + class TestConfiguration(BaseConfiguration): LOG_BACKEND = 'console' diff --git a/manage.py b/manage.py index ba06e1c7..4fabf12b 100644 --- a/manage.py +++ b/manage.py @@ -34,7 +34,6 @@ from module_build_service import models from module_build_service.pdc import ( get_pdc_client_session, get_module, get_module_runtime_dependencies, get_module_tag, get_module_build_dependencies) -import module_build_service.auth import module_build_service.scheduler.main from module_build_service.utils import ( submit_module_build, @@ -290,7 +289,6 @@ def runssl(host=conf.host, port=conf.port, debug=False): app.run( host=host, port=port, - request_handler=module_build_service.auth.ClientCertRequestHandler, ssl_context=ssl_ctx, debug=debug ) diff --git a/module_build_service/auth.py b/module_build_service/auth.py index 928d8336..924b5777 100644 --- a/module_build_service/auth.py +++ b/module_build_service/auth.py @@ -26,40 +26,73 @@ from werkzeug.serving import WSGIRequestHandler from module_build_service.errors import Unauthorized +from module_build_service import app, log import fedora.client +import httplib2 +import json +from six.moves.urllib.parse import urlencode +def _json_loads(content): + if not isinstance(content, str): + content = content.decode('utf-8') + return json.loads(content) -class ClientCertRequestHandler(WSGIRequestHandler): +client_secrets = None + +def _load_secrets(): + global client_secrets + if client_secrets: + return + + if not "OIDC_CLIENT_SECRETS" in app.config: + log.warn("To support authorization, OIDC_CLIENT_SECRETS has to be set.") + return + + secrets = _json_loads(open(app.config['OIDC_CLIENT_SECRETS'], + 'r').read()) + client_secrets = list(secrets.values())[0] + +def get_token_info(token): """ - WSGIRequestHandler subclass adding SSL_CLIENT_CERT_* variables - to `request.environ` dict when the client certificate is set and - is signed by CA configured in `conf.ssl_ca_certificate_file`. + Asks the token_introspection_uri for the validity of a token. + """ + if not client_secrets: + return None + + request = {'token': token, + 'token_type_hint': 'Bearer', + 'client_id': client_secrets['client_id'], + 'client_secret': client_secrets['client_secret']} + headers = {'Content-type': 'application/x-www-form-urlencoded'} + + resp, content = httplib2.Http().request( + client_secrets['token_introspection_uri'], 'POST', + urlencode(request), headers=headers) + + return _json_loads(content) + +def get_username(request): + """ + Returns the client's username based on the OIDC token provided. """ - def make_environ(self): - environ = WSGIRequestHandler.make_environ(self) + _load_secrets() - try: - cert = self.request.getpeercert(False) - except AttributeError: - cert = None + if not "oidc_token" in request.cookies: + raise Unauthorized("Cannot verify OIDC token.") - if cert and "subject" in cert: - for keyval in cert["subject"]: - key, val = keyval[0] - environ["SSL_CLIENT_CERT_" + key] = val - return environ + token = request.cookies["oidc_token"] + data = get_token_info(token) + if not data: + raise Unauthorized("Cannot verify OIDC token.") + if not "active" in data or not data["active"]: + raise Unauthorized("OIDC token invalid or expired.") -def get_username(environ): - """ Extract the user's username from the WSGI environment. """ - - if not "SSL_CLIENT_CERT_commonName" in environ: - raise Unauthorized("No SSL client cert CN could be found to work with") - - return environ["SSL_CLIENT_CERT_commonName"] - + #TODO: Once we will get our own scope registered in Fedora infra, + # we can start checking it here. + return data["username"] def assert_is_packager(username, fas_kwargs): """ Assert that a user is a packager by consulting FAS. diff --git a/module_build_service/views.py b/module_build_service/views.py index 1463e4ed..bd04e189 100644 --- a/module_build_service/views.py +++ b/module_build_service/views.py @@ -62,7 +62,6 @@ api_v1 = { }, } - class ModuleBuildAPI(MethodView): def get(self, id): @@ -93,7 +92,7 @@ class ModuleBuildAPI(MethodView): raise NotFound('No such module found.') def post(self): - username = module_build_service.auth.get_username(request.environ) + username = module_build_service.auth.get_username(request) if conf.require_packager: module_build_service.auth.assert_is_packager(username, fas_kwargs=dict( @@ -127,7 +126,7 @@ class ModuleBuildAPI(MethodView): return jsonify(module.json()), 201 def patch(self, id): - username = module_build_service.auth.get_username(request.environ) + username = module_build_service.auth.get_username(request) if conf.require_packager: module_build_service.auth.assert_is_packager( diff --git a/submit-build.sh b/submit-build.sh deleted file mode 100755 index 5cf6b3cb..00000000 --- a/submit-build.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -e - -MBS_HOST=${MBS_HOST:-localhost:5000} - -echo "Submitting a build of..." -cat submit-build.json -echo "Using https://$MBS_HOST/module_build_service/module-builds/" -echo "NOTE: You need to be a Fedora packager for this to work" -echo -curl --cert ~/.fedora.cert -k -H "Content-Type: text/json" --data @submit-build.json https://$MBS_HOST/module-build-service/1/module-builds/ -echo diff --git a/submit_build.py b/submit_build.py new file mode 100644 index 00000000..9ad3a413 --- /dev/null +++ b/submit_build.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +import socket +import os +import sys + +def listen_for_token(): + """ + Listens on port 13747 on localhost for a redirect request by OIDC + server, parses the response and returns the "access_token" value. + """ + TCP_IP = '127.0.0.1' + TCP_PORT = 13747 + BUFFER_SIZE = 1024 + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((TCP_IP, TCP_PORT)) + s.listen(1) + + conn, addr = s.accept() + print 'Connection address:', addr + data = "" + sent_resp = False + while 1: + try: + r = conn.recv(BUFFER_SIZE) + except: + conn.close() + break + if not r: break + data += r + + if not sent_resp: + response = "Token has been handled." + conn.send("""HTTP/1.1 200 OK +Content-Length: %s +Content-Type: text/plain +Connection: Closed + +%s""" % (len(response), response)) + conn.close() + sent_resp = True + + s.close() + + data = data.split("\n") + for line in data: + variables = line.split("&") + for var in variables: + kv = var.split("=") + if not len(kv) == 2: + continue + if kv[0] == "access_token": + return kv[1] + return None + +mbs_host = "localhost:5000" +token = None +if len(sys.argv) > 2: + token = sys.argv[2] +if len(sys.argv) > 1: + mbs_host = sys.argv[1] + +print "Usage: submit_build.py [mbs_host] [oidc_token]" +print "" +if not token: + print "Provide token as command line argument or visit following URL to obtain the token:" + print "https://id.stg.fedoraproject.org/openidc/Authorization?response_type=token&response_mode=form_post&nonce=1234&scope=openid%20profile%20email&client_id=mbs-authorizer&state=af0ifjsldkj&redirect_uri=http://localhost:13747/" + print "We are waiting for you to finish the token generation..." + +if not token: + token = listen_for_token() +if not token: + print "Failed to get a token from response" + os._exit(1) + +print "Submitting build of ..." +with open("submit-build.json", "r") as build: + print build.read() +print "Using https://%s/module_build_service/module-builds/" % mbs_host +print "NOTE: You need to be a Fedora packager for this to work" +print + +os.system("curl -b 'oidc_token=%s' -k -H 'Content-Type: text/json' --data @submit-build.json https://%s/module-build-service/1/module-builds/ -v" % (token, mbs_host)) diff --git a/tests/test_auth.py b/tests/test_auth.py index c209932a..ed97b177 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -25,6 +25,7 @@ from nose.tools import raises, eq_ import unittest import mock +from mock import patch import module_build_service.auth import module_build_service.errors @@ -32,14 +33,34 @@ import module_build_service.errors class TestAuthModule(unittest.TestCase): @raises(module_build_service.errors.Unauthorized) - def test_get_username_failure(self): - module_build_service.auth.get_username({}) + def test_get_username_no_token(self): + request = mock.MagicMock() + request.cookies.return_value = {} + module_build_service.auth.get_username(request) - def test_get_username_good(self): + @raises(module_build_service.errors.Unauthorized) + @patch('module_build_service.auth.get_token_info') + def test_get_username_failure(self, get_token_info): + def mocked_get_token_info(token): + return {"active": False} + get_token_info.return_value = mocked_get_token_info + + request = mock.MagicMock() + request.cookies.return_value = {"oidc_token", "1234"} + module_build_service.auth.get_username(request) + + @raises(module_build_service.errors.Unauthorized) + @patch('module_build_service.auth.get_token_info') + def test_get_username_good(self, get_token_info): # https://www.youtube.com/watch?v=G-LtddOgUCE name = "Joey Jo Jo Junior Shabadoo" - environ = {'SSL_CLIENT_CERT_commonName': name} - result = module_build_service.auth.get_username(environ) + def mocked_get_token_info(token): + return {"active": True, "username": name} + get_token_info.return_value = mocked_get_token_info + + request = mock.MagicMock() + request.cookies.return_value = {"oidc_token", "1234"} + result = module_build_service.auth.get_username(request) eq_(result, name) @mock.patch('fedora.client.AccountSystem') diff --git a/tests/test_views/test_views.py b/tests/test_views/test_views.py index 2e8142a3..c330b7f7 100644 --- a/tests/test_views/test_views.py +++ b/tests/test_views/test_views.py @@ -254,14 +254,14 @@ class TestViews(unittest.TestCase): self.assertEquals(data['id'], 31) self.assertEquals(data['state_name'], 'wait') - def test_submit_build_cert_error(self): + def test_submit_build_auth_error(self): rv = self.client.post('/module-build-service/1/module-builds/', data=json.dumps( {'scmurl': 'git://pkgs.stg.fedoraproject.org/modules/' 'testmodule.git?#48932b90de214d9d13feefbd35246a81b6cb8d49'})) data = json.loads(rv.data) self.assertEquals( data['message'], - 'No SSL client cert CN could be found to work with' + 'Cannot verify OIDC token.' ) self.assertEquals(data['status'], 401) self.assertEquals(data['error'], 'Unauthorized')