From 36faac7040b2d4a63ac9e3cb4a70cfd0b343f152 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: Thu, 7 Jul 2016 14:35:47 +0200 Subject: [PATCH] Add simple auth based on the client certificate and pkgdb API. --- cacert.pem | 61 ++++++++++++++++++++++++++++++++++++++ rida.conf | 6 ++++ rida.py | 17 +++++++++-- rida/auth.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ rida/config.py | 42 ++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 cacert.pem create mode 100644 rida/auth.py diff --git a/cacert.pem b/cacert.pem new file mode 100644 index 00000000..3c0062b7 --- /dev/null +++ b/cacert.pem @@ -0,0 +1,61 @@ +-----BEGIN CERTIFICATE----- +MIIK6zCCBt+gAwIBAgIJAMXcvWMyB9ZeMA0GCSqGSIb3DQEBBQUAMIGxMQswCQYD +VQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcTB1JhbGVp +Z2gxFzAVBgNVBAoTDkZlZG9yYSBQcm9qZWN0MRowGAYDVQQLExFGZWRvcmEgUHJv +amVjdCBDQTEaMBgGA1UEAxMRRmVkb3JhIFByb2plY3QgQ0ExJjAkBgkqhkiG9w0B +CQEWF2FkbWluQGZlZG9yYXByb2plY3Qub3JnMB4XDTA4MDgyMDE0NDkxNloXDTE4 +MDgxODE0NDkxNlowgbExCzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJv +bGluYTEQMA4GA1UEBxMHUmFsZWlnaDEXMBUGA1UEChMORmVkb3JhIFByb2plY3Qx +GjAYBgNVBAsTEUZlZG9yYSBQcm9qZWN0IENBMRowGAYDVQQDExFGZWRvcmEgUHJv +amVjdCBDQTEmMCQGCSqGSIb3DQEJARYXYWRtaW5AZmVkb3JhcHJvamVjdC5vcmcw +ggQWMA0GCSqGSIb3DQEBAQUAA4IEAwAwggP+AoID9QDIH2F1s0y5V7xBc2tHlXOA +H7999QZ76BU1qtDg4g4k2KyYTG7Gk5eNnJntbpYtRNPL0bQymJIhcfkMCER+UOfv +mum6hrwYSrb0ehsIP1mY9QXdJnlvA1ViXMpZy74byaue9Rn+9GOaOtRWv9dZ5/j4 +Wf9JDOt7TzgFfTPZrtasqlSaOicWJuAKyp2SkQup3I0fTtM4/LpR6BY+dDr7ud9d +LTukkGuOPnNx1pxKkuN0jKYwZjwUcQHlRUNF5xrARU5youYSD7ReWdJsZkirJ0W2 +dZkUQaIUm55v3p4soMYnbPeJFoAbSJkqSCPI4c/ex/Xr1xp3dXvd0vi9K+w8tvw1 +Q3XUvQxum97dbcM7Sw3gRfpFy6K3Up+xXaEnMDGhX31zQAHFTP/P7N+CWNwLg57r +EmuYVfP31b6qsyvuLnpMqe0fYRNWOiJYMALPyRT15RSFGaLyKevqqzR5DFmHQI2C +wl5UFsmBK4LJWqaxE/shuNWEx70BzRYOnPgPr3ohXKBLLxZZtVSlEh+N5FW07Y7T +LkzFGxc0uArsi6EsA9AS0rGJ7FOqMNctvQoR3UFPh5bkXMHgz7aunrB1n5x5rmHk +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 +YPF6KxAYMs2ema7PGl2D +-----END CERTIFICATE----- diff --git a/rida.conf b/rida.conf index 9d3bcbd8..3d9df27b 100644 --- a/rida.conf +++ b/rida.conf @@ -10,3 +10,9 @@ rpms_default_repository = git://pkgs.stg.fedoraproject.org/rpms/ rpms_allow_repository = False rpms_default_cache = http://pkgs.stg.fedoraproject.org/repo/pkgs/ rpms_allow_cache = False + +ssl_certificate_file=server.crt +ssl_certificate_key_file=server.key +ssl_ca_certificate_file=cacert.pem + +pkgdb_api_url=https://admin.stg.fedoraproject.org/pkgdb/api diff --git a/rida.py b/rida.py index 6849a00c..6d8c1100 100755 --- a/rida.py +++ b/rida.py @@ -36,9 +36,10 @@ This is the implementation of the orchestrator's public RESTful API. # TODO: Emit messages about module submission. from flask import Flask, request -from rida import config, database, messaging +from rida import config, database, messaging, auth import json import modulemd +import ssl app = Flask(__name__) app.config.from_envvar("RIDA_SETTINGS", silent=True) @@ -50,6 +51,12 @@ db = database.Database() @app.route("/rida/module-builds/", methods=["POST"]) def submit_build(): """Handles new module build submissions.""" + + username = auth.is_packager(conf.pkgdb_api_url) + if not username: + return ("You must use your Fedora certificate when submitting" + " new build", 403) + try: r = json.loads(request.data.decode('utf-8')) except: @@ -126,4 +133,10 @@ def query_build(id): return "No such module found.", 404 if __name__ == "__main__": - app.run() + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + ssl_ctx.load_cert_chain(conf.ssl_certificate_file, + conf.ssl_certificate_key_file) + ssl_ctx.verify_mode = ssl.CERT_OPTIONAL + ssl_ctx.load_verify_locations(cafile=conf.ssl_ca_certificate_file) + + app.run(request_handler=auth.ClientCertRequestHander, ssl_context=ssl_ctx) diff --git a/rida/auth.py b/rida/auth.py new file mode 100644 index 00000000..33acfda3 --- /dev/null +++ b/rida/auth.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Jan Kaluza + +"""Auth system based on the client certificate and FAS account""" + +from flask import Flask, request +from werkzeug.serving import WSGIRequestHandler +import requests +import json + +class ClientCertRequestHander(WSGIRequestHandler): + """ + 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`. + """ + + def make_environ(self): + environ = WSGIRequestHandler.make_environ(self) + + try: + cert = self.request.getpeercert(False) + except: + cert = None + + if cert and "subject" in cert: + for keyval in cert["subject"]: + key, val = keyval[0] + environ["SSL_CLIENT_CERT_" + key] = val + return environ + +def is_packager(pkgdb_api_url): + """ + Returns the username of user associated with current request by checking + client cert's commonName and pkgdb database API. + + When user is not a packager (is not in pkgdb), returns None. + """ + if not "SSL_CLIENT_CERT_commonName" in request.environ: + return None + + username = request.environ["SSL_CLIENT_CERT_commonName"] + + acl_url = pkgdb_api_url + "/packager/package/" + username + + resp = requests.get(acl_url) + try: + resp.raise_for_status() + except: + return None + + try: + r = json.loads(resp.content.decode('utf-8')) + except: + return None + + if r["output"] == "ok": + return username + + return None diff --git a/rida/config.py b/rida/config.py index 4f3a0df8..85fbd241 100644 --- a/rida/config.py +++ b/rida/config.py @@ -56,6 +56,12 @@ def from_file(filename=None): conf.rpms_allow_repository = default.getboolean("rpms_allow_repository") conf.rpms_default_cache = default.get("rpms_default_cache") conf.rpms_allow_cache = default.getboolean("rpms_allow_cache") + + conf.ssl_certificate_file = default.get("ssl_certificate_file") + conf.ssl_certificate_key_file = default.get("ssl_certificate_key_file") + conf.ssl_ca_certificate_file = default.get("ssl_ca_certificate_file") + + conf.pkgdb_api_url = default.get("pkgdb_api_url") return conf class Config(object): @@ -72,6 +78,10 @@ class Config(object): self._rpms_allow_repository = False self._rpms_default_cache = "" self._rpms_allow_cache = False + self._ssl_certificate_file = "" + self._ssl_certificate_key_file = "" + self._ssl_ca_certificate_file = "" + self._pkgdb_api_url = "" @property def system(self): @@ -170,3 +180,35 @@ class Config(object): if not isinstance(b, bool): raise TypeError("rpms_allow_cache must be a bool.") self._rpms_allow_cache = b + + @property + def ssl_certificate_file(self): + return self._ssl_certificate_file + + @ssl_certificate_file.setter + def ssl_certificate_file(self, s): + self._ssl_certificate_file = str(s) + + @property + def ssl_ca_certificate_file(self): + return self._ssl_ca_certificate_file + + @ssl_ca_certificate_file.setter + def ssl_ca_certificate_file(self, s): + self._ssl_ca_certificate_file = str(s) + + @property + def ssl_certificate_key_file(self): + return self._ssl_certificate_key_file + + @ssl_certificate_key_file.setter + def ssl_certificate_key_file(self, s): + self._ssl_certificate_key_file = str(s) + + @property + def pkgdb_api_url(self): + return self._pkgdb_api_url + + @pkgdb_api_url.setter + def pkgdb_api_url(self, s): + self._pkgdb_api_url = str(s)