Use OIDC to auth the users, replace submit-build.sh by submit-build.py which does hackish way of OIDC just to test things.

This commit is contained in:
Jan Kaluza
2016-12-02 14:39:14 +01:00
parent d453352f84
commit 8cb4e0de5d
8 changed files with 172 additions and 46 deletions

View File

@@ -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'

View File

@@ -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
)

View File

@@ -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.

View File

@@ -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(

View File

@@ -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

84
submit_build.py Normal file
View File

@@ -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))

View File

@@ -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')

View File

@@ -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')