diff --git a/files/aclchecker.py b/files/aclchecker.py index fc442da..e389c87 100644 --- a/files/aclchecker.py +++ b/files/aclchecker.py @@ -15,6 +15,8 @@ import subprocess import sys import os +import requests + if "SSH_ORIGINAL_COMMAND" not in os.environ: print("Welcome %s. This server does not offer ssh access." % sys.argv[1]) sys.exit(0) @@ -27,9 +29,6 @@ if "PAGURE_CONFIG" not in os.environ and os.path.exists( os.environ["PAGURE_CONFIG"] = "/etc/pagure/pagure.cfg" # Here starts the code -import pagure -import pagure.lib -from pagure.utils import is_repo_user from pagure.config import config as pagure_config @@ -47,73 +46,56 @@ if len(args) != 2: cmd = args[0] -path = args[1] +gitdir = args[1] if cmd not in ("git-receive-pack", "git-upload-pack"): print("Invalid call, invalid operation", file=sys.stderr) sys.exit(1) +# Normalization of the gitdir # Git will encode the file path argument within single quotes -if path[0] != "'" or path[-1] != "'": +if gitdir[0] != "'" or gitdir[-1] != "'": print("Invalid call: invalid path", file=sys.stderr) sys.exit(1) -path = path[1:-1] -if path[0] == '/': - # With the "ssh://hostname/repo.git", SSH sends "/repo.git" - path = path[1:] - -if os.path.isabs(path): - print("Non-full path expected, not %s" % path, file=sys.stderr) +gitdir = gitdir[1:-1] +# With the "ssh://hostname/repo.git", SSH sends "/repo.git" +if gitdir[0] == "/": + gitdir = gitdir[1:] +# Always add .git for good measure +if not gitdir.endswith(".git"): + gitdir = gitdir + ".git" + + +url = "%s/pv/ssh/checkaccess/" % pagure_config["APP_URL"] +data = {"gitdir": gitdir, "username": remoteuser} +resp = requests.post(url, data=data) +if not resp.status_code == 200: + print( + "Error during lookup request: status: %s" % resp.status_code, + file=sys.stderr, + ) sys.exit(1) -if not path.endswith(".git"): - path = path + ".git" - -session = pagure.lib.create_session(pagure_config["DB_URL"]) -if not session: - raise Exception("Unable to initialize db session") +result = resp.json() -gitdir = os.path.join(pagure_config["GIT_FOLDER"], path) -(repotype, username, namespace, repo) = pagure.lib.git.get_repo_info_from_path( - gitdir, hide_notfound=True -) - -if repo is None: - print("Repo not found", file=sys.stderr) +if not result["access"]: + # The user does not have access to this repo, or project does + # not exist. Whatever it is, no access. + print("No such repository") sys.exit(1) -project = pagure.lib.get_authorized_project( - session, repo, user=username, namespace=namespace, asuser=remoteuser -) - -if not project: - print("Repo not found", file=sys.stderr) - sys.exit(1) - -if repotype != "main" and not is_repo_user(project, remoteuser): - print("Repo not found", file=sys.stderr) - sys.exit(1) # Now go run the configured command # We verified that cmd is either "git-receive-pack" or "git-send-pack" # and "path" is a path that points to a valid Pagure repository. -if project.is_on_repospanner: +if result["region"]: runner, env = pagure_config["SSH_COMMAND_REPOSPANNER"] else: runner, env = pagure_config["SSH_COMMAND_NON_REPOSPANNER"] -runenv = { - "username": remoteuser, - "cmd": cmd, - "reponame": path, - "repopath": gitdir, - "repotype": repotype, - "region": project.repospanner_region, - "project_name": project.name, - "project_user": project.user if project.is_fork else '', - "project_namespace": project.namespace, -} -runargs = [arg % runenv for arg in runner] +result.update({"username": remoteuser, "cmd": cmd}) + +runargs = [arg % result for arg in runner] if env: for key in env: - os.environ[key] = env[key] % runenv + os.environ[key] = env[key] % result os.execvp(runargs[0], runargs) diff --git a/pagure/internal/__init__.py b/pagure/internal/__init__.py index 67932c7..9509f4a 100644 --- a/pagure/internal/__init__.py +++ b/pagure/internal/__init__.py @@ -33,6 +33,7 @@ import pagure.lib.git # noqa: E402 import pagure.lib.tasks # noqa: E402 import pagure.utils # noqa: E402 import pagure.ui.fork # noqa: E402 +from pagure.config import config as pagure_config # noqa: E402 _log = logging.getLogger(__name__) @@ -66,7 +67,7 @@ def localonly(function): def decorated_function(*args, **kwargs): """ Wrapped function actually checking if the request is local. """ - ip_allowed = pagure.config.config.get( + ip_allowed = pagure_config.get( "IP_ALLOWED_INTERNAL", ["127.0.0.1", "localhost", "::1"] ) if flask.request.remote_addr not in ip_allowed: @@ -107,6 +108,55 @@ def lookup_ssh_key(): return flask.jsonify(result) +@PV.route("/ssh/checkaccess/", methods=["POST"]) +@localonly +def check_ssh_access(): + """ Determines whether a user has any access to the requested repo. """ + gitdir = flask.request.form["gitdir"] + remoteuser = flask.request.form["username"] + + # Build a fake path so we can use get_repo_info_from_path + path = os.path.join(pagure_config["GIT_FOLDER"], gitdir) + ( + repotype, + project_user, + namespace, + repo, + ) = pagure.lib.git.get_repo_info_from_path(path, hide_notfound=True) + + if repo is None: + return flask.jsonify({"access": False}) + + project = pagure.lib.get_authorized_project( + flask.g.session, + repo, + user=project_user, + namespace=namespace, + asuser=remoteuser, + ) + + if not project: + return flask.jsonify({"access": False}) + + if repotype != "main" and not pagure.utils.is_repo_user( + project, remoteuser + ): + return flask.jsonify({"access": False}) + + return flask.jsonify( + { + "access": True, + "reponame": gitdir, + "repopath": path, + "repotype": repotype, + "region": project.repospanner_region, + "project_name": project.name, + "project_user": project.user.username if project.is_fork else None, + "project_namespace": project.namespace, + } + ) + + @PV.route("/pull-request/comment/", methods=["PUT"]) @localonly def pull_request_add_comment(): diff --git a/pagure/lib/git.py b/pagure/lib/git.py index cad3259..cfc5598 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -1282,10 +1282,10 @@ def get_repo_info_from_path(gitdir, hide_notfound=False): gitdir = os.path.normpath(gitdir) types = { - "main": os.path.abspath(pagure_config["GIT_FOLDER"]), - "docs": os.path.abspath(pagure_config["DOCS_FOLDER"]), - "tickets": os.path.abspath(pagure_config["TICKETS_FOLDER"]), - "requests": os.path.abspath(pagure_config["REQUESTS_FOLDER"]), + "main": pagure_config["GIT_FOLDER"], + "docs": pagure_config["DOCS_FOLDER"], + "tickets": pagure_config["TICKETS_FOLDER"], + "requests": pagure_config["REQUESTS_FOLDER"], } match = None @@ -1295,6 +1295,9 @@ def get_repo_info_from_path(gitdir, hide_notfound=False): # non-main repos are in a subdir of main (i.e. repos/ and repos/tickets/), # we find the correct type. for typename in types: + if not types[typename]: + continue + types[typename] = os.path.abspath(types[typename]) path = types[typename] + "/" if gitdir.startswith(path) and ( matchlen is None or len(path) > matchlen @@ -1360,11 +1363,8 @@ def get_repo_info_from_path(gitdir, hide_notfound=False): "Rebuilt %s path not identical to gitdir %s" % (rebuiltpath, gitdir) ) - if not os.path.exists(rebuiltpath): - if hide_notfound: - return (None, None, None, None) - else: - raise ValueError("Splitting gitdir %s failed" % gitdir) + if not os.path.exists(rebuiltpath) and not hide_notfound: + raise ValueError("Splitting gitdir %s failed" % gitdir) return (repotype, username, namespace, repo) diff --git a/tests/test_pagure_flask_internal.py b/tests/test_pagure_flask_internal.py index 402e3c4..9346bb4 100644 --- a/tests/test_pagure_flask_internal.py +++ b/tests/test_pagure_flask_internal.py @@ -47,9 +47,6 @@ class PagureFlaskInternaltests(tests.Modeltests): pagure.config.config['GIT_FOLDER'] = os.path.join( self.path, 'repos') - pagure.config.config['REQUESTS_FOLDER'] = None - pagure.config.config['TICKETS_FOLDER'] = None - pagure.config.config['DOCS_FOLDER'] = None @patch('pagure.lib.notify.send_email') def test_pull_request_add_comment(self, send_email): @@ -2954,6 +2951,44 @@ class PagureFlaskInternaltests(tests.Modeltests): self.assertEqual(result['username'], 'deploykey_test_2') self.assertEqual(result['public_key'], project_key) + def test_check_ssh_access(self): + """ Test the SSH access check endpoint. """ + tests.create_projects(self.session) + + url = '/pv/ssh/checkaccess/' + def runtest(project, username, access): + output = self.app.post( + url, + data={ + 'gitdir': project, + 'username': username, + } + ) + self.assertEqual(output.status_code, 200) + result = json.loads(output.get_data(as_text=True)) + self.assertEqual(result['access'], access) + return result + + runtest('project.git', 'pingou', False) + i1 = runtest('test.git', 'pingou', True) + i2 = runtest('test.git', 'foo', True) + i3 = runtest('tickets/test.git', 'pingou', True) + runtest('tickets/test.git', 'foo', False) + + self.assertEqual(i1['reponame'], 'test.git') + self.assertEqual(i1['repotype'], 'main') + self.assertEqual(i1['region'], None) + self.assertEqual(i1['project_name'], 'test') + self.assertEqual(i1['project_user'], None) + self.assertEqual(i1['project_namespace'], None) + self.assertEqual(i1, i2) + self.assertEqual(i3['reponame'], 'tickets/test.git') + self.assertEqual(i3['repotype'], 'tickets') + self.assertEqual(i3['region'], None) + self.assertEqual(i3['project_name'], 'test') + self.assertEqual(i3['project_user'], None) + self.assertEqual(i3['project_namespace'], None) + if __name__ == '__main__': unittest.main(verbosity=2)