mirror of
https://pagure.io/fm-orchestrator.git
synced 2026-02-13 10:05:15 +08:00
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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
84
submit_build.py
Normal 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))
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user