contentdb/app/blueprints/github/__init__.py

150 lines
5.0 KiB
Python
Raw Normal View History

2020-07-12 17:34:25 +02:00
# ContentDB
2021-01-30 17:59:42 +01:00
# Copyright (C) 2018-21 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/>.
2020-01-24 19:15:09 +01:00
from flask import Blueprint
2018-05-17 16:18:20 +02:00
2020-01-24 19:15:09 +01:00
bp = Blueprint("github", __name__)
2021-01-26 17:22:13 +01:00
from flask import redirect, url_for, request, flash, jsonify, current_app
from flask_login import current_user
from sqlalchemy import func, or_, and_
2020-01-24 22:39:35 +01:00
from app import github, csrf
2020-12-15 13:29:25 +01:00
from app.models import db, User, APIToken, Package, Permission, AuditSeverity
2021-01-26 17:22:13 +01:00
from app.utils import abs_url_for, addAuditLog, login_user_set_active
2020-01-24 22:39:35 +01:00
from app.blueprints.api.support import error, handleCreateRelease
2021-01-26 17:22:13 +01:00
import hmac, requests
2018-03-18 19:05:53 +01:00
2020-01-24 19:15:09 +01:00
@bp.route("/github/start/")
def start():
2020-01-25 18:23:14 +01:00
return github.authorize("", redirect_uri=abs_url_for("github.callback"))
2018-03-18 19:05:53 +01:00
@bp.route("/github/view/")
def view_permissions():
url = "https://github.com/settings/connections/applications/" + \
current_app.config["GITHUB_CLIENT_ID"]
return redirect(url)
2020-01-24 22:39:35 +01:00
@bp.route("/github/callback/")
2018-03-18 19:05:53 +01:00
@github.authorized_handler
2020-01-24 19:15:09 +01:00
def callback(oauth_token):
2018-03-21 23:03:37 +01:00
next_url = request.args.get("next")
2018-03-18 19:05:53 +01:00
if oauth_token is None:
flash("Authorization failed [err=gh-oauth-login-failed]", "danger")
return redirect(url_for("users.login"))
2018-03-18 19:05:53 +01:00
# Get Github username
url = "https://api.github.com/user"
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
username = r.json()["login"]
# Get user by github username
userByGithub = User.query.filter(func.lower(User.github_username) == func.lower(username)).first()
2018-03-18 19:05:53 +01:00
# If logged in, connect
if current_user and current_user.is_authenticated:
if userByGithub is None:
current_user.github_username = username
db.session.commit()
2018-07-30 01:42:11 +02:00
flash("Linked github to account", "success")
2019-11-21 20:38:26 +01:00
return redirect(url_for("homepage.home"))
2018-03-18 19:05:53 +01:00
else:
flash("Github account is already associated with another user", "danger")
2019-11-21 20:38:26 +01:00
return redirect(url_for("homepage.home"))
2018-03-18 19:05:53 +01:00
# If not logged in, log in
else:
if userByGithub is None:
2020-01-24 19:15:09 +01:00
flash("Unable to find an account for that Github user", "danger")
2020-12-22 11:58:43 +01:00
return redirect(url_for("users.claim_forums"))
2020-12-15 13:29:25 +01:00
elif login_user_set_active(userByGithub, remember=True):
2020-12-09 21:38:36 +01:00
addAuditLog(AuditSeverity.USER, userByGithub, "Logged in using GitHub OAuth",
url_for("users.profile", username=userByGithub.username))
db.session.commit()
2020-12-04 23:35:22 +01:00
if not current_user.password:
return redirect(next_url or url_for("users.set_password", optional=True))
else:
2019-11-21 20:38:26 +01:00
return redirect(next_url or url_for("homepage.home"))
2018-03-18 19:05:53 +01:00
else:
flash("Authorization failed [err=gh-login-failed]", "danger")
return redirect(url_for("users.login"))
2020-01-24 22:39:35 +01:00
@bp.route("/github/webhook/", methods=["POST"])
@csrf.exempt
def webhook():
json = request.json
# Get package
github_url = "github.com/" + json["repository"]["full_name"]
package = Package.query.filter(Package.repo.ilike("%{}%".format(github_url))).first()
2020-01-24 22:39:35 +01:00
if package is None:
return error(400, "Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(github_url))
2020-01-24 22:39:35 +01:00
# 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()
2020-01-24 22:39:35 +01:00
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")
2020-01-24 22:39:35 +01:00
2020-01-25 01:04:56 +01:00
if not package.checkPerm(actual_token.owner, Permission.APPROVE_RELEASE):
return error(403, "You do not have the permission to approve releases")
2020-01-25 01:04:56 +01:00
2020-01-24 22:39:35 +01:00
#
# Check event
#
event = request.headers.get("X-GitHub-Event")
if event == "push":
ref = json["after"]
2020-01-25 02:14:01 +01:00
title = json["head_commit"]["message"].partition("\n")[0]
elif event == "create" and json["ref_type"] == "tag":
ref = json["ref"]
title = ref
2020-01-25 00:19:06 +01:00
elif event == "ping":
return jsonify({ "success": True, "message": "Ping successful" })
2020-01-24 22:39:35 +01:00
else:
2020-01-25 02:14:01 +01:00
return error(400, "Unsupported event. Only 'push', `create:tag`, and 'ping' are supported.")
2020-01-24 22:39:35 +01:00
#
# Perform release
#
return handleCreateRelease(actual_token, package, title, ref)