mirror of
https://github.com/minetest/contentdb.git
synced 2024-12-22 14:02:24 +01:00
parent
ca961cb35f
commit
09e06a159a
@ -1,86 +0,0 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import datetime
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
bp = Blueprint("gitlab", __name__)
|
||||
|
||||
from app import csrf
|
||||
from app.models import Package, APIToken, Permission, PackageState
|
||||
from app.blueprints.api.support import error, api_create_vcs_release
|
||||
|
||||
|
||||
def webhook_impl():
|
||||
json = request.json
|
||||
|
||||
# Get package
|
||||
gitlab_url = json["project"]["web_url"].replace("https://", "").replace("http://", "")
|
||||
package = Package.query.filter(
|
||||
Package.repo.ilike("%{}%".format(gitlab_url)), Package.state != PackageState.DELETED).first()
|
||||
if package is None:
|
||||
return error(400,
|
||||
"Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(gitlab_url))
|
||||
|
||||
# Get all tokens for package
|
||||
secret = request.headers.get("X-Gitlab-Token")
|
||||
if secret is None:
|
||||
return error(403, "Token required")
|
||||
|
||||
token = APIToken.query.filter_by(access_token=secret).first()
|
||||
if token is None:
|
||||
return error(403, "Invalid authentication")
|
||||
|
||||
if not package.check_perm(token.owner, Permission.APPROVE_RELEASE):
|
||||
return error(403, "You do not have the permission to approve releases")
|
||||
|
||||
#
|
||||
# Check event
|
||||
#
|
||||
|
||||
event = json["event_name"]
|
||||
if event == "push":
|
||||
ref = json["after"]
|
||||
title = datetime.datetime.utcnow().strftime("%Y-%m-%d") + " " + ref[:5]
|
||||
branch = json["ref"].replace("refs/heads/", "")
|
||||
if branch not in ["master", "main"]:
|
||||
return jsonify({"success": False,
|
||||
"message": "Webhook ignored, as it's not on the master/main branch"})
|
||||
|
||||
elif event == "tag_push":
|
||||
ref = json["ref"]
|
||||
title = ref.replace("refs/tags/", "")
|
||||
else:
|
||||
return error(400, "Unsupported event: '{}'. Only 'push', 'create:tag', and 'ping' are supported."
|
||||
.format(event or "null"))
|
||||
|
||||
#
|
||||
# Perform release
|
||||
#
|
||||
|
||||
if package.releases.filter_by(commit_hash=ref).count() > 0:
|
||||
return
|
||||
|
||||
return api_create_vcs_release(token, package, title, ref, reason="Webhook")
|
||||
|
||||
|
||||
@bp.route("/gitlab/webhook/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def webhook():
|
||||
try:
|
||||
return webhook_impl()
|
||||
except KeyError as err:
|
||||
return error(400, "Missing field: {}".format(err.args[0]))
|
@ -50,7 +50,7 @@ def claim_forums():
|
||||
flash(gettext("Unable to get GitHub username for user"), "danger")
|
||||
return redirect(url_for("users.claim_forums", username=username))
|
||||
else:
|
||||
return redirect(url_for("github.start"))
|
||||
return redirect(url_for("vcs.github_start"))
|
||||
|
||||
if "forum_token" in session:
|
||||
token = session["forum_token"]
|
||||
|
22
app/blueprints/vcs/__init__.py
Normal file
22
app/blueprints/vcs/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2024 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("vcs", __name__)
|
||||
|
||||
from . import github, gitlab
|
42
app/blueprints/vcs/common.py
Normal file
42
app/blueprints/vcs/common.py
Normal file
@ -0,0 +1,42 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2024 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from app.blueprints.api.support import error
|
||||
from app.models import Package, APIToken, Permission, PackageState
|
||||
|
||||
|
||||
def get_packages_for_vcs_and_token(token: APIToken, repo_url: str) -> list[Package]:
|
||||
if token.package:
|
||||
packages = [token.package]
|
||||
if not token.package.check_perm(token.owner, Permission.APPROVE_RELEASE):
|
||||
return error(403, "You do not have the permission to approve releases")
|
||||
|
||||
actual_repo_url: str = token.package.repo or ""
|
||||
if repo_url not in actual_repo_url.lower():
|
||||
return error(400, "Repo URL does not match the API token's package")
|
||||
else:
|
||||
# Get package
|
||||
packages = Package.query.filter(
|
||||
Package.repo.ilike("%{}%".format(repo_url)), Package.state != PackageState.DELETED).all()
|
||||
if len(packages) == 0:
|
||||
return error(400,
|
||||
"Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(repo_url))
|
||||
packages = [x for x in packages if x.check_perm(token.owner, Permission.APPROVE_RELEASE)]
|
||||
if len(packages) == 0:
|
||||
return error(403, "You do not have the permission to approve releases")
|
||||
|
||||
return packages
|
@ -1,5 +1,5 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
# Copyright (C) 2018-24 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
@ -15,33 +15,35 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import hmac
|
||||
|
||||
from flask import Blueprint, abort, Response
|
||||
from flask_babel import gettext
|
||||
from app.logic.users import create_user
|
||||
import requests
|
||||
from flask import abort, Response
|
||||
from flask import redirect, url_for, request, flash, jsonify, current_app
|
||||
from flask_babel import gettext
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import or_, and_
|
||||
from app import github, csrf
|
||||
from app.models import db, User, APIToken, Package, Permission, AuditSeverity, PackageState
|
||||
from app.utils import abs_url_for, add_audit_log, login_user_set_active, is_safe_url
|
||||
from app.blueprints.api.support import error, api_create_vcs_release
|
||||
import hmac, requests
|
||||
|
||||
bp = Blueprint("github", __name__)
|
||||
from app import github, csrf
|
||||
from app.blueprints.api.support import error, api_create_vcs_release
|
||||
from app.logic.users import create_user
|
||||
from app.models import db, User, APIToken, AuditSeverity
|
||||
from app.utils import abs_url_for, add_audit_log, login_user_set_active, is_safe_url
|
||||
|
||||
from . import bp
|
||||
from .common import get_packages_for_vcs_and_token
|
||||
|
||||
|
||||
@bp.route("/github/start/")
|
||||
def start():
|
||||
def github_start():
|
||||
next = request.args.get("next")
|
||||
if next and not is_safe_url(next):
|
||||
abort(400)
|
||||
|
||||
return github.authorize("", redirect_uri=abs_url_for("github.callback", next=next))
|
||||
return github.authorize("", redirect_uri=abs_url_for("vcs.github_callback", next=next))
|
||||
|
||||
|
||||
@bp.route("/github/view/")
|
||||
def view_permissions():
|
||||
def github_view_permissions():
|
||||
url = "https://github.com/settings/connections/applications/" + \
|
||||
current_app.config["GITHUB_CLIENT_ID"]
|
||||
return redirect(url)
|
||||
@ -49,7 +51,7 @@ def view_permissions():
|
||||
|
||||
@bp.route("/github/callback/")
|
||||
@github.authorized_handler
|
||||
def callback(oauth_token):
|
||||
def github_callback(oauth_token):
|
||||
if oauth_token is None:
|
||||
flash(gettext("Authorization failed [err=gh-oauth-login-failed]"), "danger")
|
||||
return redirect(url_for("users.login"))
|
||||
@ -125,61 +127,43 @@ def callback(oauth_token):
|
||||
return ret
|
||||
|
||||
|
||||
def _find_api_token(header_signature: str) -> APIToken:
|
||||
sha_name, signature = header_signature.split('=')
|
||||
if sha_name != 'sha1':
|
||||
error(403, "Expected SHA1 payload signature")
|
||||
|
||||
for token in APIToken.query.all():
|
||||
mac = hmac.new(token.access_token.encode("utf-8"), msg=request.data, digestmod='sha1')
|
||||
|
||||
if hmac.compare_digest(str(mac.hexdigest()), signature):
|
||||
return token
|
||||
|
||||
error(401, "Invalid authentication, couldn't validate API token")
|
||||
|
||||
|
||||
@bp.route("/github/webhook/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def webhook():
|
||||
def github_webhook():
|
||||
json = request.json
|
||||
|
||||
# Get package
|
||||
github_url = "github.com/" + json["repository"]["full_name"]
|
||||
package = Package.query.filter(
|
||||
Package.repo.ilike("%{}%".format(github_url)), Package.state != PackageState.DELETED).first()
|
||||
if package is None:
|
||||
return error(400, "Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(github_url))
|
||||
|
||||
# Get all tokens for package
|
||||
tokens_query = APIToken.query.filter(or_(APIToken.package==package,
|
||||
and_(APIToken.package==None, APIToken.owner==package.author)))
|
||||
|
||||
possible_tokens = tokens_query.all()
|
||||
actual_token = None
|
||||
|
||||
#
|
||||
# Check signature
|
||||
#
|
||||
|
||||
header_signature = request.headers.get('X-Hub-Signature')
|
||||
if header_signature is None:
|
||||
return error(403, "Expected payload signature")
|
||||
|
||||
sha_name, signature = header_signature.split('=')
|
||||
if sha_name != 'sha1':
|
||||
return error(403, "Expected SHA1 payload signature")
|
||||
|
||||
for token in possible_tokens:
|
||||
mac = hmac.new(token.access_token.encode("utf-8"), msg=request.data, digestmod='sha1')
|
||||
|
||||
if hmac.compare_digest(str(mac.hexdigest()), signature):
|
||||
actual_token = token
|
||||
break
|
||||
|
||||
if actual_token is None:
|
||||
return error(403, "Invalid authentication, couldn't validate API token")
|
||||
|
||||
if not package.check_perm(actual_token.owner, Permission.APPROVE_RELEASE):
|
||||
return error(403, "You do not have the permission to approve releases")
|
||||
token = _find_api_token(header_signature)
|
||||
packages = get_packages_for_vcs_and_token(token, "github.com/" + json["repository"]["full_name"])
|
||||
|
||||
for package in packages:
|
||||
#
|
||||
# Check event
|
||||
#
|
||||
|
||||
event = request.headers.get("X-GitHub-Event")
|
||||
if event == "push":
|
||||
ref = json["after"]
|
||||
title = datetime.datetime.utcnow().strftime("%Y-%m-%d") + " " + ref[:5]
|
||||
branch = json["ref"].replace("refs/heads/", "")
|
||||
if branch not in [ "master", "main" ]:
|
||||
return jsonify({ "success": False, "message": "Webhook ignored, as it's not on the master/main branch" })
|
||||
continue
|
||||
|
||||
elif event == "create":
|
||||
ref_type = json.get("ref_type")
|
||||
@ -193,7 +177,7 @@ def webhook():
|
||||
title = ref
|
||||
|
||||
elif event == "ping":
|
||||
return jsonify({ "success": True, "message": "Ping successful" })
|
||||
return jsonify({"success": True, "message": "Ping successful"})
|
||||
|
||||
else:
|
||||
return error(400, "Unsupported event: '{}'. Only 'push', 'create:tag', and 'ping' are supported."
|
||||
@ -202,8 +186,12 @@ def webhook():
|
||||
#
|
||||
# Perform release
|
||||
#
|
||||
|
||||
if package.releases.filter_by(commit_hash=ref).count() > 0:
|
||||
return
|
||||
|
||||
return api_create_vcs_release(actual_token, package, title, ref, reason="Webhook")
|
||||
return api_create_vcs_release(token, package, title, ref, reason="Webhook")
|
||||
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "No release made. Either the release already exists or the event was filtered based on the branch"
|
||||
})
|
81
app/blueprints/vcs/gitlab.py
Normal file
81
app/blueprints/vcs/gitlab.py
Normal file
@ -0,0 +1,81 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020-24 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import request, jsonify
|
||||
|
||||
from app import csrf
|
||||
from app.blueprints.api.support import error, api_create_vcs_release
|
||||
from app.models import APIToken
|
||||
|
||||
from . import bp
|
||||
from .common import get_packages_for_vcs_and_token
|
||||
|
||||
|
||||
def webhook_impl():
|
||||
json = request.json
|
||||
|
||||
# Get all tokens for package
|
||||
secret = request.headers.get("X-Gitlab-Token")
|
||||
if secret is None:
|
||||
return error(403, "Token required")
|
||||
|
||||
token: APIToken = APIToken.query.filter_by(access_token=secret).first()
|
||||
if token is None:
|
||||
return error(403, "Invalid authentication")
|
||||
|
||||
packages = get_packages_for_vcs_and_token(token, json["project"]["web_url"].replace("https://", "").replace("http://", ""))
|
||||
for package in packages:
|
||||
#
|
||||
# Check event
|
||||
#
|
||||
event = json["event_name"]
|
||||
if event == "push":
|
||||
ref = json["after"]
|
||||
title = datetime.datetime.utcnow().strftime("%Y-%m-%d") + " " + ref[:5]
|
||||
branch = json["ref"].replace("refs/heads/", "")
|
||||
if branch not in ["master", "main"]:
|
||||
continue
|
||||
elif event == "tag_push":
|
||||
ref = json["ref"]
|
||||
title = ref.replace("refs/tags/", "")
|
||||
else:
|
||||
return error(400, "Unsupported event: '{}'. Only 'push', 'create:tag', and 'ping' are supported."
|
||||
.format(event or "null"))
|
||||
|
||||
#
|
||||
# Perform release
|
||||
#
|
||||
if package.releases.filter_by(commit_hash=ref).count() > 0:
|
||||
continue
|
||||
|
||||
return api_create_vcs_release(token, package, title, ref, reason="Webhook")
|
||||
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "No release made. Either the release already exists or the event was filtered based on the branch"
|
||||
})
|
||||
|
||||
|
||||
|
||||
@bp.route("/gitlab/webhook/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def gitlab_webhook():
|
||||
try:
|
||||
return webhook_impl()
|
||||
except KeyError as err:
|
||||
return error(400, "Missing field: {}".format(err.args[0]))
|
@ -55,7 +55,7 @@
|
||||
</p>
|
||||
|
||||
{% if user == current_user %}
|
||||
<a class="btn btn-secondary" href="{{ url_for('github.view_permissions') }}">
|
||||
<a class="btn btn-secondary" href="{{ url_for('vcs.github_view_permissions') }}">
|
||||
{{ _("View ContentDB's GitHub Permissions") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -66,7 +66,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
{% elif user == current_user %}
|
||||
<a class="btn btn-secondary" href="{{ url_for('github.start') }}">
|
||||
<a class="btn btn-secondary" href="{{ url_for('vcs.github_start') }}">
|
||||
{{ _("Link Github") }}
|
||||
</a>
|
||||
{% else %}
|
||||
|
@ -25,7 +25,7 @@
|
||||
<hr class="my-5" />
|
||||
|
||||
<p>
|
||||
<a class="btn btn-secondary me-3" href="{{ url_for('github.start', next=next) }}">
|
||||
<a class="btn btn-secondary me-3" href="{{ url_for('vcs.github_start', next=next) }}">
|
||||
<i class="fab fa-github me-1"></i>
|
||||
{{ _("GitHub") }}
|
||||
</a>
|
||||
|
Loading…
Reference in New Issue
Block a user