Update for sr.ht unified config

This commit is contained in:
Drew DeVault 2018-09-03 14:17:51 -04:00
parent bcb14b332d
commit 84560e5fa0
14 changed files with 162 additions and 255 deletions

68
config.example.ini Normal file
View File

@ -0,0 +1,68 @@
[sr.ht]
#
# The name of your network of sr.ht-based sites
site-name=sr.ht
#
# Contact information for the site owners
owner-name=Drew DeVault
owner-email=sir@cmpwn.com
#
# The source code for your fork of sr.ht
source-url=https://git.sr.ht/~sircmpwn/srht
#
# A secret key to encrypt session cookies with
secret-key=CHANGEME
[mail]
#
# Outgoing SMTP settings
smtp-host=
smtp-port=
smtp-user=
smtp-password=
smtp-from=
#
# Application exceptions are emailed to this address
error-to=
error-from=
#
# Your PGP key information (DO NOT mix up pub and priv here)
# You must remove the password from your secret key, if present.
# You can do this with gpg --edit-key [key-id], then use the passwd
# command and do not enter a new password.
pgp-privkey=
pgp-pubkey=
pgp-key-id=
[dispatch.sr.ht]
#
# URL dispatch.sr.ht is being served at (protocol://domain)
origin=http://dispatch.sr.ht.local
#
# Address and port to bind the debug server to
debug-host=0.0.0.0
debug-port=5005
#
# Configures the SQLAlchemy connection string for the database.
connection-string=postgresql://postgres@localhost/dispatch.sr.ht
#
# dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
oauth-client-id=
oauth-client-secret=
[dispatch.sr.ht::github]
#
# Fill this in with a registered GitHub OAuth client to enable GitHub
# integration
oauth-client-id=
oauth-client-secret=
[builds.sr.ht]
origin=http://builds.sr.ht.local
#
# Fill this in with your builds.sr.ht OAuth client ID
oauth-client-id=
[meta.sr.ht]
origin=http://meta.sr.ht.local

View File

@ -1,63 +0,0 @@
#
# dispatch.sr.ht config
[server]
#
# Specifies the protocol (usually http or https) dispatch.sr.ht runs with.
protocol=http
#
# Specifies the domain name dispatch.sr.ht is running on.
domain=dispatch.sr.ht.local
#
# A secret key to encrypt session cookies with.
secret-key=CHANGEME
[debug]
#
# Address and port to bind the debug server to.
debug-host=0.0.0.0
debug-port=5005
[sr.ht]
#
# Configures the SQLAlchemy connection string for the database.
connection-string=postgresql://postgres@localhost/dispatch.sr.ht
#
# The name of your network of sr.ht-based sites
site-name=sr.ht
[network]
#
# Location of other sites in your network
#
# This isn't a hardcoded list, add or remove entries as you like. The upstream
# sites do know about each other and will omit integrations if you leave out
# the relevant site. Only meta is required.
meta=http://meta.sr.ht.local
git=http://git.sr.ht.local
builds=http://builds.sr.ht.local
todo=http://todo.sr.ht.local
dispatch=http://dispatch.sr.ht.local
[meta.sr.ht]
oauth-client-id=CHANGEME
oauth-client-secret=CHANGEME
[builds.sr.ht]
#
# Fill this in with the oauth client ID builds.sr.ht uses for builds.sr.ht
# integration
oauth-client-id=
[github]
#
# Fill this in with a registered GitHub OAuth client to enable GitHub
# integration
#
# When you reigster the client, set the callback URL to:
#
# https://dispatch.sr.ht/github/callback
#
# Of course, replace the domain with your own.
oauth-client-id=
oauth-client-secret=

View File

@ -1,59 +1,58 @@
from flask import render_template, request
from flask_login import LoginManager, current_user
from jinja2 import Markup
import locale
import urllib
from srht.config import cfg, cfgi, load_config
load_config("dispatch")
from srht.flask import SrhtFlask
from srht.config import cfg
from srht.database import DbSession
db = DbSession(cfg("sr.ht", "connection-string"))
db = DbSession(cfg("dispatch.sr.ht", "connection-string"))
from dispatchsrht.types import User
db.init()
from srht.flask import SrhtFlask
app = SrhtFlask("dispatch", __name__)
app.secret_key = cfg("server", "secret-key")
login_manager = LoginManager()
login_manager.init_app(app)
class DispatchApp(SrhtFlask):
def __init__(self):
super().__init__("dispatch.sr.ht", __name__)
@login_manager.user_loader
def load_user(username):
return User.query.filter(User.username == username).first()
from dispatchsrht.blueprints.html import html
self.register_blueprint(html)
login_manager.anonymous_user = lambda: None
meta_client_id = cfg("dispatch.sr.ht", "oauth-client-id")
meta_client_secret = cfg("dispatch.sr.ht", "oauth-client-secret")
builds_sr_ht = cfg("builds.sr.ht", "oauth-client-id")
self.configure_meta_auth(meta_client_id, meta_client_secret,
base_scopes=["profile", "keys"] + [
builds_sr_ht + "/jobs:write"
] if builds_sr_ht else [])
try:
locale.setlocale(locale.LC_ALL, 'en_US')
except:
pass
@self.login_manager.user_loader
def user_loader(username):
# TODO: Switch to a session token
return User.query.filter(User.username == username).one_or_none()
builds_sr_ht = cfg("builds.sr.ht", "oauth-client-id")
@self.context_processor
def inject():
from dispatchsrht.tasks import taskdefs
return { "taskdefs": taskdefs }
def oauth_url(return_to):
return "{}/oauth/authorize?client_id={}&scopes=profile{}&state={}".format(
meta_sr_ht, meta_client_id,
"," + builds_sr_ht + "/jobs:write" if builds_sr_ht else "",
urllib.parse.quote_plus(return_to))
def register_tasks(self):
# This is done in a separate step because we can't import these right
# away due to a circular dependency on the app variable
from dispatchsrht.tasks import taskdefs
for taskdef in taskdefs():
self.register_blueprint(taskdef.blueprint,
url_prefix="/" + taskdef.name)
from dispatchsrht.blueprints.html import html
from dispatchsrht.blueprints.auth import auth
def lookup_or_register(self, exchange, profile, scopes):
user = User.query.filter(User.username == profile["username"]).first()
if not user:
user = User()
db.session.add(user)
user.username = profile.get("username")
user.email = profile.get("email")
user.oauth_token = exchange["token"]
user.oauth_token_expires = exchange["expires"]
user.oauth_token_scopes = scopes
db.session.commit()
return user
app.register_blueprint(html)
app.register_blueprint(auth)
from dispatchsrht.tasks import taskdefs
for taskdef in taskdefs():
app.register_blueprint(taskdef.blueprint, url_prefix="/" + taskdef.name)
meta_sr_ht = cfg("network", "meta")
meta_client_id = cfg("meta.sr.ht", "oauth-client-id")
@app.context_processor
def inject():
return {
"oauth_url": oauth_url(request.full_path),
"current_user": User.query.filter(User.id == current_user.id).first() \
if current_user else None,
"taskdefs": taskdefs
}
app = DispatchApp()
app.register_tasks()

View File

@ -1,81 +0,0 @@
from flask import Blueprint, request, render_template, redirect
from flask_login import login_user, logout_user
from sqlalchemy import or_
from srht.config import cfg
from srht.flask import DATE_FORMAT
from srht.oauth import OAuthScope
from srht.database import db
from dispatchsrht.types import User
from datetime import datetime
import urllib.parse
import requests
auth = Blueprint('auth', __name__)
meta_uri = cfg("network", "meta")
client_id = cfg("meta.sr.ht", "oauth-client-id")
client_secret = cfg("meta.sr.ht", "oauth-client-secret")
@auth.route("/oauth/callback")
def oauth_callback():
error = request.args.get("error")
if error:
details = request.args.get("details")
return render_template("oauth-error.html", details=details)
exchange = request.args.get("exchange")
scopes = request.args.get("scopes")
state = request.args.get("state")
_scopes = [OAuthScope(s) for s in scopes.split(",")]
if not OAuthScope("profile:read") in _scopes:
return render_template("oauth-error.html",
details="dispatch.sr.ht requires profile and key access at a mininum to function correctly. " +
"To use dispatch.sr.ht, try again and do not untick these permissions.")
if not exchange:
return render_template("oauth-error.html",
details="Expected an exchange token from meta.sr.ht. Something odd has happened, try again.")
r = requests.post(meta_uri + "/oauth/exchange", json={
"client_id": client_id,
"client_secret": client_secret,
"exchange": exchange,
})
if r.status_code != 200:
return render_template("oauth-error.html",
details="Error occured retrieving OAuth token. Try again.")
json = r.json()
token = json.get("token")
expires = json.get("expires")
if not token or not expires:
return render_template("oauth-error.html",
details="Error occured retrieving OAuth token. Try again.")
expires = datetime.strptime(expires, DATE_FORMAT)
r = requests.get(meta_uri + "/api/user/profile", headers={
"Authorization": "token " + token
})
if r.status_code != 200:
return render_template("oauth-error.html",
details="Error occured retrieving account info. Try again.")
json = r.json()
user = User.query.filter(or_(User.oauth_token == token,
User.username == json["username"])).first()
if not user:
user = User()
db.session.add(user)
user.username = json.get("username")
user.email = json.get("email")
user.oauth_token = token
user.oauth_token_expires = expires
user.oauth_token_scopes = scopes
db.session.commit()
login_user(user, remember=True)
if not state or not state.startswith("/"):
return redirect("/")
else:
return redirect(urllib.parse.unquote(state))
@auth.route("/logout")
def logout():
logout_user()
return redirect(request.headers.get("Referer") or "/")

View File

@ -1,13 +1,13 @@
from flask import Blueprint, render_template
from flask_login import current_user
from srht.config import cfg
from dispatchsrht.decorators import loginrequired
from srht.flask import loginrequired
from dispatchsrht.types import Task
import requests
html = Blueprint('html', __name__)
meta_uri = cfg("network", "meta")
meta_uri = cfg("meta.sr.ht", "origin")
@html.route("/")
def index():

View File

@ -1,15 +0,0 @@
from flask import redirect, request, abort
from flask_login import current_user
from functools import wraps
from dispatchsrht.app import oauth_url
import urllib
def loginrequired(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user:
return redirect(oauth_url(request.url))
else:
return f(*args, **kwargs)
return wrapper

View File

@ -13,9 +13,9 @@ from flask_login import current_user
from functools import wraps
from github import Github, GithubException
from urllib.parse import urlencode
from srht.database import Base, db
from srht.config import cfg
from dispatchsrht.decorators import loginrequired
from srht.database import Base, db
from srht.flask import loginrequired
from dispatchsrht.app import app
def _first_line(text):
@ -23,8 +23,10 @@ def _first_line(text):
return text
return text[:text.index("\n") + 1]
_github_client_id = cfg("github", "oauth-client-id", default=None)
_github_client_secret = cfg("github", "oauth-client-secret", default=None)
_github_client_id = cfg("dispatch.sr.ht::github",
"oauth-client-id", default=None)
_github_client_secret = cfg("dispatch.sr.ht::github",
"oauth-client-secret", default=None)
class GitHubAuthorization(Base):
__tablename__ = "github_authorization"
@ -92,9 +94,9 @@ def github_callback():
db.session.commit()
return redirect(state)
_root = "{}://{}".format(cfg("server", "protocol"), cfg("server", "domain"))
_builds_sr_ht = cfg("network", "builds", default=None)
_secret_key = cfg("server", "secret-key")
_root = cfg("dispatch.sr.ht", "origin")
_builds_sr_ht = cfg("builds.sr.ht", "origin", default=None)
_secret_key = cfg("sr.ht", "secret-key")
_kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,

View File

@ -7,29 +7,31 @@ from jinja2 import Markup
from uuid import UUID, uuid4
from srht.database import Base, db
from srht.config import cfg
from srht.flask import icon
from srht.validation import Validation
from dispatchsrht.tasks import TaskDef
from dispatchsrht.tasks.github.auth import githubloginrequired, GitHubAuthorization
from dispatchsrht.tasks.github.auth import GitHubAuthorization
from dispatchsrht.tasks.github.auth import githubloginrequired
from dispatchsrht.tasks.github.auth import submit_build
from dispatchsrht.types import Task
_root = "{}://{}".format(cfg("server", "protocol"), cfg("server", "domain"))
_builds_sr_ht = cfg("network", "builds", default=None)
_github_client_id = cfg("github", "oauth-client-id", default=None)
_github_client_secret = cfg("github", "oauth-client-secret", default=None)
_root = cfg("dispatch.sr.ht", "origin")
_builds_sr_ht = cfg("builds.sr.ht", "origin", default=None)
_github_client_id = cfg("dispatch.sr.ht::github",
"oauth-client-id", default=None)
_github_client_secret = cfg("dispatch.sr.ht::github",
"oauth-client-secret", default=None)
class GitHubCommitToBuild(TaskDef):
name = "github_commit_to_build"
description = Markup('''
<i class="fa fa-github"></i>
GitHub commits
<i class="fa fa-arrow-right"></i>
builds.sr.ht jobs
''')
enabled = bool(_github_client_id
and _github_client_secret
and _builds_sr_ht)
def description():
return (icon("github") + Markup(" GitHub commits ") +
icon("caret-right") + Markup(" builds.sr.ht jobs"))
class _GitHubCommitToBuildRecord(Base):
__tablename__ = "github_commit_to_build"
id = sa.Column(sau.UUIDType, primary_key=True)

View File

@ -7,29 +7,31 @@ from jinja2 import Markup
from uuid import UUID, uuid4
from srht.database import Base, db
from srht.config import cfg
from srht.flask import icon
from srht.validation import Validation
from dispatchsrht.tasks import TaskDef
from dispatchsrht.tasks.github.auth import githubloginrequired, GitHubAuthorization
from dispatchsrht.tasks.github.auth import GitHubAuthorization
from dispatchsrht.tasks.github.auth import githubloginrequired
from dispatchsrht.tasks.github.auth import submit_build
from dispatchsrht.types import Task
_root = "{}://{}".format(cfg("server", "protocol"), cfg("server", "domain"))
_builds_sr_ht = cfg("network", "builds", default=None)
_github_client_id = cfg("github", "oauth-client-id", default=None)
_github_client_secret = cfg("github", "oauth-client-secret", default=None)
_root = cfg("dispatch.sr.ht", "origin")
_builds_sr_ht = cfg("builds.sr.ht", "origin", default=None)
_github_client_id = cfg("dispatch.sr.ht::github",
"oauth-client-id", default=None)
_github_client_secret = cfg("dispatch.sr.ht::github",
"oauth-client-secret", default=None)
class GitHubPRToBuild(TaskDef):
name = "github_pr_to_build"
description = Markup('''
<i class="fa fa-github"></i>
GitHub pull requests
<i class="fa fa-arrow-right"></i>
builds.sr.ht jobs
''')
enabled = bool(_github_client_id
and _github_client_secret
and _builds_sr_ht)
def description():
return (icon("github") + Markup(" GitHub pull requests ") +
icon("caret-right") + Markup(" builds.sr.ht jobs"))
class _GitHubPRToBuildRecord(Base):
__tablename__ = "github_pr_to_build"
id = sa.Column(sau.UUIDType, primary_key=True)

View File

@ -26,10 +26,9 @@
href="/{{ task.name }}/configure"
class="btn btn-primary btn-sm pull-right"
>
Configure task
<i class="fa fa-caret-right"></i>
Configure task {{icon("caret-right")}}
</a>
{{ task.description }}
{{ task.description() }}
</h4>
<div class="clearfix"></div>
</div>

View File

@ -15,10 +15,7 @@
<a
href="/configure"
class="btn btn-primary btn-block"
>
Configure new task
<i class="fa fa-caret-right"></i>
</a>
>Configure new task {{icon("caret-right")}}</a>
</div>
<div class="col-md-8">
{% if not any(tasks) %}
@ -44,7 +41,7 @@
</a>
</h4>
<p>
{{ task.taskdef.description }}
{{ task.taskdef.description() }}
</p>
</div>
{% endfor %}

View File

@ -26,15 +26,12 @@
<form class="event" method="POST">
<input type="hidden" name="repo" value="{{ repo.full_name }}" />
<h4>
<i class="fa fa-github"></i>
{{icon("github")}}
{% if repo.full_name not in existing %}
<button
type="submit"
class="pull-right btn btn-primary btn-lg"
>
Add task
<i class="fa fa-caret-right"></i>
</button>
>Add task {{icon("caret-right")}}</button>
{% else %}
<button
class="pull-right btn btn-default btn-lg"
@ -46,7 +43,7 @@
href="{{ repo.html_url }}"
target="_blank"
rel="noopener"
><i class="fa fa-external-link"></i></a>
>{{icon("external-link-alt")}}</a>
</h4>
</form>
{% endfor %}

View File

@ -6,7 +6,7 @@
<h2>dispatch</h2>
<p>
Welcome to dispatch.sr.ht. This is a part of the
<a href="{{cfg("network", "meta")}}">
<a href="{{cfg("meta.sr.ht", "origin")}}">
{{cfg("sr.ht", "site-name")}} network
</a>
and provides integration services to members.

4
run.py
View File

@ -6,6 +6,6 @@ import os
app.static_folder = os.path.join(os.getcwd(), "static")
if __name__ == '__main__':
app.run(host=cfg("debug", "debug-host"),
port=cfgi("debug", "debug-port"),
app.run(host=cfg("dispatch.sr.ht", "debug-host"),
port=cfgi("dispatch.sr.ht", "debug-port"),
debug=True)