2020-07-12 17:34:25 +02:00
|
|
|
# ContentDB
|
2018-05-17 16:18:20 +02:00
|
|
|
# Copyright (C) 2018 rubenwardy
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU 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 General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# 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__)
|
|
|
|
|
2020-01-30 22:39:51 +01:00
|
|
|
from flask import redirect, url_for, request, flash, abort, render_template, jsonify, current_app
|
2020-12-15 13:29:25 +01:00
|
|
|
from flask_login import current_user, login_required
|
2020-04-11 16:24:44 +02:00
|
|
|
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
|
|
|
|
from app.utils import randomString, abs_url_for, addAuditLog, login_user_set_active
|
2020-01-24 22:39:35 +01:00
|
|
|
from app.blueprints.api.support import error, handleCreateRelease
|
2020-01-25 00:19:06 +01:00
|
|
|
import hmac, requests, json
|
|
|
|
|
|
|
|
from flask_wtf import FlaskForm
|
|
|
|
from wtforms import SelectField, SubmitField
|
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
|
|
|
|
2020-01-30 22:39:51 +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")
|
2020-12-04 23:05:10 +01:00
|
|
|
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
|
2018-05-23 19:22:41 +02:00
|
|
|
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:
|
2019-11-16 00:51:42 +01:00
|
|
|
return redirect(next_url or url_for("users.set_password", optional=True))
|
2018-06-04 19:49:42 +02:00
|
|
|
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")
|
2020-12-04 23:05:10 +01:00
|
|
|
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"]
|
2020-06-03 17:32:39 +02:00
|
|
|
package = Package.query.filter(Package.repo.ilike("%{}%".format(github_url))).first()
|
2020-01-24 22:39:35 +01:00
|
|
|
if package is None:
|
2020-06-03 17:32:39 +02:00
|
|
|
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
|
2020-04-11 16:24:44 +02:00
|
|
|
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:
|
2020-04-11 16:24:44 +02:00
|
|
|
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):
|
2020-04-21 20:27:34 +02:00
|
|
|
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)
|
2020-01-25 00:19:06 +01:00
|
|
|
|
|
|
|
|
|
|
|
class SetupWebhookForm(FlaskForm):
|
2020-01-25 18:25:05 +01:00
|
|
|
event = SelectField("Event Type", choices=[('create', 'New tag or GitHub release'), ('push', 'Push')])
|
2020-01-25 00:19:06 +01:00
|
|
|
submit = SubmitField("Save")
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/github/callback/webhook/")
|
|
|
|
@github.authorized_handler
|
|
|
|
def callback_webhook(oauth_token=None):
|
|
|
|
pid = request.args.get("pid")
|
|
|
|
if pid is None:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
current_user.github_access_token = oauth_token
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
return redirect(url_for("github.setup_webhook", pid=pid))
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/github/webhook/new/", methods=["GET", "POST"])
|
|
|
|
@login_required
|
|
|
|
def setup_webhook():
|
|
|
|
pid = request.args.get("pid")
|
|
|
|
if pid is None:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
package = Package.query.get(pid)
|
|
|
|
if package is None:
|
|
|
|
abort(404)
|
|
|
|
|
2020-01-25 01:04:56 +01:00
|
|
|
if not package.checkPerm(current_user, Permission.APPROVE_RELEASE):
|
|
|
|
flash("Only trusted members can use webhooks", "danger")
|
|
|
|
return redirect(package.getDetailsURL())
|
|
|
|
|
2020-01-25 00:19:06 +01:00
|
|
|
gh_user, gh_repo = package.getGitHubFullName()
|
|
|
|
if gh_user is None or gh_repo is None:
|
|
|
|
flash("Unable to get Github full name from repo address", "danger")
|
|
|
|
return redirect(package.getDetailsURL())
|
|
|
|
|
|
|
|
if current_user.github_access_token is None:
|
2020-12-04 03:23:04 +01:00
|
|
|
return github.authorize("write:repo_hook",
|
|
|
|
redirect_uri=abs_url_for("github.callback_webhook", pid=pid))
|
2020-01-25 00:19:06 +01:00
|
|
|
|
|
|
|
form = SetupWebhookForm(formdata=request.form)
|
2020-12-05 00:07:19 +01:00
|
|
|
if form.validate_on_submit():
|
2020-01-25 00:19:06 +01:00
|
|
|
token = APIToken()
|
2020-01-25 18:25:05 +01:00
|
|
|
token.name = "GitHub Webhook for " + package.title
|
2020-01-25 00:19:06 +01:00
|
|
|
token.owner = current_user
|
|
|
|
token.access_token = randomString(32)
|
|
|
|
token.package = package
|
|
|
|
|
|
|
|
event = form.event.data
|
2020-01-25 02:31:39 +01:00
|
|
|
if event != "push" and event != "create":
|
2020-01-25 00:19:06 +01:00
|
|
|
abort(500)
|
|
|
|
|
2020-12-04 03:23:04 +01:00
|
|
|
if handleMakeWebhook(gh_user, gh_repo, package,
|
2020-01-25 02:53:02 +01:00
|
|
|
current_user.github_access_token, event, token):
|
2020-01-25 18:25:05 +01:00
|
|
|
flash("Successfully created webhook", "success")
|
2020-01-25 00:19:06 +01:00
|
|
|
return redirect(package.getDetailsURL())
|
|
|
|
else:
|
2020-01-25 02:53:02 +01:00
|
|
|
return redirect(url_for("github.setup_webhook", pid=package.id))
|
2020-01-25 00:19:06 +01:00
|
|
|
|
2020-12-04 03:23:04 +01:00
|
|
|
return render_template("github/setup_webhook.html",
|
|
|
|
form=form, package=package)
|
2020-01-25 02:53:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
def handleMakeWebhook(gh_user, gh_repo, package, oauth, event, token):
|
|
|
|
url = "https://api.github.com/repos/{}/{}/hooks".format(gh_user, gh_repo)
|
|
|
|
headers = {
|
|
|
|
"Authorization": "token " + oauth
|
|
|
|
}
|
|
|
|
data = {
|
|
|
|
"name": "web",
|
|
|
|
"active": True,
|
|
|
|
"events": [event],
|
|
|
|
"config": {
|
2020-01-25 04:03:45 +01:00
|
|
|
"url": abs_url_for("github.webhook"),
|
2020-01-25 02:53:02 +01:00
|
|
|
"content_type": "json",
|
|
|
|
"secret": token.access_token
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
# First check that the webhook doesn't already exist
|
|
|
|
r = requests.get(url, headers=headers)
|
|
|
|
|
|
|
|
if r.status_code == 401 or r.status_code == 403:
|
|
|
|
current_user.github_access_token = None
|
|
|
|
db.session.commit()
|
|
|
|
return False
|
|
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
flash("Failed to create webhook, received response from Github " +
|
|
|
|
str(r.status_code) + ": " +
|
|
|
|
str(r.json().get("message")), "danger")
|
|
|
|
return False
|
|
|
|
|
|
|
|
for hook in r.json():
|
2020-01-25 04:09:59 +01:00
|
|
|
if hook.get("config") and hook["config"].get("url") and \
|
|
|
|
hook["config"]["url"] == data["config"]["url"]:
|
2020-01-25 02:53:02 +01:00
|
|
|
flash("Failed to create webhook, as it already exists", "danger")
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# Create it
|
|
|
|
r = requests.post(url, headers=headers, data=json.dumps(data))
|
|
|
|
|
|
|
|
if r.status_code == 201:
|
|
|
|
db.session.add(token)
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
elif r.status_code == 401 or r.status_code == 403:
|
|
|
|
current_user.github_access_token = None
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
else:
|
2020-12-14 22:22:11 +01:00
|
|
|
import sys
|
|
|
|
print(r.text, file=sys.stderr)
|
|
|
|
|
|
|
|
message = str(r.status_code) + ": " + str(r.json().get("message"))
|
|
|
|
flash("Failed to create webhook, Github says: " + message, "danger")
|
2020-01-25 02:53:02 +01:00
|
|
|
return False
|