2020-07-12 17:34:25 +02:00
|
|
|
# ContentDB
|
2024-06-22 12:42:51 +02:00
|
|
|
# Copyright (C) 2018-24 rubenwardy
|
2018-05-17 16:18:20 +02:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
2021-01-30 17:59:42 +01:00
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
2018-05-17 16:18:20 +02:00
|
|
|
# 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
|
2021-01-30 17:59:42 +01:00
|
|
|
# GNU Affero General Public License for more details.
|
2018-05-17 16:18:20 +02:00
|
|
|
#
|
2021-01-30 17:59:42 +01:00
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
2018-05-17 16:18:20 +02:00
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2024-03-06 00:59:30 +01:00
|
|
|
import datetime
|
2024-06-22 12:42:51 +02:00
|
|
|
import hmac
|
2024-03-06 00:59:30 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
import requests
|
|
|
|
from flask import abort, Response
|
2021-01-26 17:22:13 +01:00
|
|
|
from flask import redirect, url_for, request, flash, jsonify, current_app
|
2024-06-22 12:42:51 +02:00
|
|
|
from flask_babel import gettext
|
2021-01-26 17:22:13 +01:00
|
|
|
from flask_login import current_user
|
2024-06-22 12:42:51 +02:00
|
|
|
|
2020-01-24 22:39:35 +01:00
|
|
|
from app import github, csrf
|
2021-02-02 01:07:41 +01:00
|
|
|
from app.blueprints.api.support import error, api_create_vcs_release
|
2024-06-22 12:42:51 +02:00
|
|
|
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
|
2018-03-18 19:05:53 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
from . import bp
|
|
|
|
from .common import get_packages_for_vcs_and_token
|
2024-06-02 22:17:15 +02:00
|
|
|
|
2023-10-31 19:45:24 +01:00
|
|
|
|
2020-01-24 19:15:09 +01:00
|
|
|
@bp.route("/github/start/")
|
2024-06-22 12:42:51 +02:00
|
|
|
def github_start():
|
2023-10-31 19:45:24 +01:00
|
|
|
next = request.args.get("next")
|
|
|
|
if next and not is_safe_url(next):
|
|
|
|
abort(400)
|
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
return github.authorize("", redirect_uri=abs_url_for("vcs.github_callback", next=next))
|
2023-10-31 19:45:24 +01:00
|
|
|
|
2018-03-18 19:05:53 +01:00
|
|
|
|
2020-01-30 22:39:51 +01:00
|
|
|
@bp.route("/github/view/")
|
2024-06-22 12:42:51 +02:00
|
|
|
def github_view_permissions():
|
2020-01-30 22:39:51 +01:00
|
|
|
url = "https://github.com/settings/connections/applications/" + \
|
|
|
|
current_app.config["GITHUB_CLIENT_ID"]
|
|
|
|
return redirect(url)
|
|
|
|
|
2023-06-18 23:21:37 +02:00
|
|
|
|
2020-01-24 22:39:35 +01:00
|
|
|
@bp.route("/github/callback/")
|
2018-03-18 19:05:53 +01:00
|
|
|
@github.authorized_handler
|
2024-06-22 12:42:51 +02:00
|
|
|
def github_callback(oauth_token):
|
2018-03-18 19:05:53 +01:00
|
|
|
if oauth_token is None:
|
2022-01-07 22:46:16 +01:00
|
|
|
flash(gettext("Authorization failed [err=gh-oauth-login-failed]"), "danger")
|
2020-12-04 23:05:10 +01:00
|
|
|
return redirect(url_for("users.login"))
|
2018-03-18 19:05:53 +01:00
|
|
|
|
2023-10-31 19:45:24 +01:00
|
|
|
next = request.args.get("next")
|
|
|
|
if next and not is_safe_url(next):
|
|
|
|
abort(400)
|
|
|
|
|
|
|
|
redirect_to = next
|
|
|
|
if redirect_to is None:
|
|
|
|
redirect_to = url_for("homepage.home")
|
|
|
|
|
2023-06-18 23:07:46 +02:00
|
|
|
# Get GitGub username
|
2018-03-18 19:05:53 +01:00
|
|
|
url = "https://api.github.com/user"
|
|
|
|
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
|
2024-03-30 17:52:17 +01:00
|
|
|
json = r.json()
|
|
|
|
user_id = json["id"]
|
2024-06-02 13:46:56 +02:00
|
|
|
github_username = json["login"]
|
2024-03-30 18:06:32 +01:00
|
|
|
if type(user_id) is not int:
|
|
|
|
abort(400)
|
2018-03-18 19:05:53 +01:00
|
|
|
|
2024-03-30 17:52:17 +01:00
|
|
|
# Get user by GitHub user ID
|
2024-06-02 13:46:56 +02:00
|
|
|
user_by_github = User.query.filter(User.github_user_id == user_id).one_or_none()
|
2018-03-18 19:05:53 +01:00
|
|
|
|
|
|
|
# If logged in, connect
|
|
|
|
if current_user and current_user.is_authenticated:
|
2024-06-02 13:46:56 +02:00
|
|
|
if user_by_github is None:
|
|
|
|
current_user.github_username = github_username
|
2024-03-30 17:52:17 +01:00
|
|
|
current_user.github_user_id = user_id
|
2018-03-18 19:05:53 +01:00
|
|
|
db.session.commit()
|
2022-01-20 02:28:50 +01:00
|
|
|
flash(gettext("Linked GitHub to account"), "success")
|
2023-10-31 19:45:24 +01:00
|
|
|
return redirect(redirect_to)
|
2024-06-02 13:46:56 +02:00
|
|
|
elif user_by_github == current_user:
|
2024-03-30 17:52:17 +01:00
|
|
|
return redirect(redirect_to)
|
2018-03-18 19:05:53 +01:00
|
|
|
else:
|
2024-04-08 00:17:34 +02:00
|
|
|
flash(gettext("GitHub account is already associated with another user: %(username)s",
|
2024-06-02 13:46:56 +02:00
|
|
|
username=user_by_github.username), "danger")
|
2023-10-31 19:45:24 +01:00
|
|
|
return redirect(redirect_to)
|
2018-03-18 19:05:53 +01:00
|
|
|
|
2024-06-02 13:46:56 +02:00
|
|
|
# Log in to existing account
|
|
|
|
elif user_by_github:
|
|
|
|
ret = login_user_set_active(user_by_github, next, remember=True)
|
|
|
|
if ret is None:
|
|
|
|
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
|
|
|
|
return redirect(url_for("users.login"))
|
|
|
|
|
|
|
|
add_audit_log(AuditSeverity.USER, user_by_github, "Logged in using GitHub OAuth",
|
|
|
|
url_for("users.profile", username=user_by_github.username))
|
|
|
|
db.session.commit()
|
|
|
|
return ret
|
|
|
|
|
|
|
|
# Sign up
|
2018-03-18 19:05:53 +01:00
|
|
|
else:
|
2024-06-02 22:17:15 +02:00
|
|
|
user = create_user(github_username, github_username, None, "GitHub")
|
|
|
|
if isinstance(user, Response):
|
|
|
|
return user
|
|
|
|
elif user is None:
|
2024-06-02 13:46:56 +02:00
|
|
|
return redirect(url_for("users.login"))
|
|
|
|
|
2024-06-04 21:29:46 +02:00
|
|
|
user.github_username = github_username
|
|
|
|
user.github_user_id = user_id
|
|
|
|
|
2024-06-02 13:46:56 +02:00
|
|
|
add_audit_log(AuditSeverity.USER, user, "Registered with GitHub, display name=" + user.display_name,
|
|
|
|
url_for("users.profile", username=user.username))
|
2020-12-09 21:38:36 +01:00
|
|
|
|
2024-06-02 13:46:56 +02:00
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
ret = login_user_set_active(user, next, remember=True)
|
2021-07-20 00:04:40 +02:00
|
|
|
if ret is None:
|
2022-01-07 22:46:16 +01:00
|
|
|
flash(gettext("Authorization failed [err=gh-login-failed]"), "danger")
|
2020-12-04 23:05:10 +01:00
|
|
|
return redirect(url_for("users.login"))
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2021-07-20 00:04:40 +02:00
|
|
|
return ret
|
|
|
|
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
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')
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
if hmac.compare_digest(str(mac.hexdigest()), signature):
|
|
|
|
return token
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
error(401, "Invalid authentication, couldn't validate API token")
|
2020-04-11 16:24:44 +02:00
|
|
|
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
@bp.route("/github/webhook/", methods=["POST"])
|
|
|
|
@csrf.exempt
|
|
|
|
def github_webhook():
|
|
|
|
json = request.json
|
2020-01-24 22:39:35 +01:00
|
|
|
|
|
|
|
header_signature = request.headers.get('X-Hub-Signature')
|
|
|
|
if header_signature is None:
|
|
|
|
return error(403, "Expected payload signature")
|
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
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/", "")
|
2024-06-22 13:13:49 +02:00
|
|
|
if package.update_config and package.update_config.ref:
|
|
|
|
if branch != package.update_config.ref:
|
|
|
|
continue
|
|
|
|
elif branch not in ["master", "main"]:
|
2024-06-22 12:42:51 +02:00
|
|
|
continue
|
|
|
|
|
|
|
|
elif event == "create":
|
|
|
|
ref_type = json.get("ref_type")
|
|
|
|
if ref_type != "tag":
|
|
|
|
return jsonify({
|
2021-12-12 18:20:23 +01:00
|
|
|
"success": False,
|
|
|
|
"message": "Webhook ignored, as it's a non-tag create event. ref_type='{}'.".format(ref_type)
|
2024-06-22 12:42:51 +02:00
|
|
|
})
|
2021-12-12 18:20:23 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
ref = json["ref"]
|
|
|
|
title = ref
|
2021-12-12 18:20:23 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
elif event == "ping":
|
|
|
|
return jsonify({"success": True, "message": "Ping successful"})
|
2021-12-12 18:20:23 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
else:
|
|
|
|
return error(400, "Unsupported event: '{}'. Only 'push', 'create:tag', and 'ping' are supported."
|
|
|
|
.format(event or "null"))
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
#
|
|
|
|
# Perform release
|
|
|
|
#
|
|
|
|
if package.releases.filter_by(commit_hash=ref).count() > 0:
|
|
|
|
return
|
2020-01-24 22:39:35 +01:00
|
|
|
|
2024-06-22 16:18:58 +02:00
|
|
|
return api_create_vcs_release(token, package, title, title, None, ref, reason="Webhook")
|
2021-02-04 20:54:26 +01:00
|
|
|
|
2024-06-22 12:42:51 +02:00
|
|
|
return jsonify({
|
|
|
|
"success": False,
|
|
|
|
"message": "No release made. Either the release already exists or the event was filtered based on the branch"
|
|
|
|
})
|