diff --git a/Procfile b/Procfile deleted file mode 100644 index d51668a..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python -m meeseeksbox diff --git a/flit.ini b/flit.ini index 39100b7..d44c580 100644 --- a/flit.ini +++ b/flit.ini @@ -4,4 +4,15 @@ author = Matthias Bussonnier author-email = bussonniermatthias@gmail.com home-page = https://github.com/Carreau/meeseeksbox classifiers = License :: OSI Approved :: MIT License +requires-python = >=3.4 +description-file = readme.md +requires = tornado + requests + pyjwt + gitpython + there + mock + cryptography + friendlyautopep8 + yieldbreaker diff --git a/meeseeksbox/__init__.py b/meeseeksbox/__init__.py index 6614e55..56518f1 100644 --- a/meeseeksbox/__init__.py +++ b/meeseeksbox/__init__.py @@ -1,7 +1,20 @@ +""" +MeeseeksBox + +Base of a framework to write stateless bots on GitHub. + +Mainly writte to use the (currently Beta) new GitHub "Integration" API, and +handle authencation of user. +""" + import os import base64 from .core import Config +version_info = (0, 0, 4) + +__version__ = '.'.join(map(str,version_info)) + def load_config_from_env(): """ Load the configuration, for now stored in the environment @@ -30,21 +43,3 @@ def load_config_from_env(): config['webhook_secret'] = os.environ.get('WEBHOOK_SECRET') return Config(**config).validate() - -from .core import MeeseeksBox -from .commands import replyuser, zen, backport, migrate_issue_request, tag, untag - -def main(): - print('====== (re) starting ======') - config = load_config_from_env() - MeeseeksBox(commands={ - 'hello': replyuser, - 'zen': zen, - 'backport': backport, - 'migrate': migrate_issue_request, - 'tag': tag, - 'untag': untag - }, config=config).start() - -if __name__ == "__main__": - main() diff --git a/meeseeksbox/__main__.py b/meeseeksbox/__main__.py deleted file mode 100644 index 031df43..0000000 --- a/meeseeksbox/__main__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import main -main() diff --git a/meeseeksbox/commands.py b/meeseeksbox/commands.py index e1e4ade..23511f7 100644 --- a/meeseeksbox/commands.py +++ b/meeseeksbox/commands.py @@ -274,6 +274,9 @@ def migrate_issue_request(*, session:Session, payload:dict, arguments:str): org, repo = arguments.split('/') target_session = yield org_repo + if not target_session: + session.post_comment(payload['issue']['comments_url'], "It appears that I can't do that") + return issue_title = payload['issue']['title'] issue_body = payload['issue']['body'] diff --git a/meeseeksbox/core.py b/meeseeksbox/core.py index ff2799f..4d19322 100644 --- a/meeseeksbox/core.py +++ b/meeseeksbox/core.py @@ -1,11 +1,13 @@ import re import os import hmac +import types import tornado.web import tornado.httpserver import tornado.ioloop from .utils import Authenticator +from .scopes import Permission from yieldbreaker import YieldBreaker @@ -85,7 +87,7 @@ def get(self): def post(self): if not 'X-Hub-Signature' in self.request.headers: return self.error('WebHook not configured with secret') - # TODO: Extract fom X-GitHub-Event + # TODO: Extract from X-GitHub-Event if not verify_signature(self.request.body, self.request.headers['X-Hub-Signature'], @@ -96,7 +98,7 @@ def post(self): payload = tornado.escape.json_decode(self.request.body) org = payload.get('repository', {}).get('owner', {}).get('login') if hasattr(self.config, 'org_whitelist') and (org not in self.config.org_whitelist): - print('Non allowed orgg:', org) + print('Non allowed org:', org) self.error('Not allowed org.') sender = payload.get('sender', {}).get('login', {}) if hasattr(self.config, 'user_whitelist') and (sender not in self.config.user_whitelist): @@ -115,7 +117,8 @@ def post(self): self.request.headers.get('X-GitHub-Delivery')) return self.dispatch_action(action, payload) else: - print('No action available for the webhook :', payload) + print('No action available for the webhook :', + self.request.headers.get('X-GitHub-Delivery'), ':', payload) @property def mention_bot_re(self): @@ -170,18 +173,17 @@ def dispatch_on_mention(self, body, payload, user): org = payload['organization']['login'] repo = payload['repository']['name'] session = self.auth.session(installation_id) - is_admin = session.is_collaborator(org, repo, user) + permission_level = session._get_permission(org, repo, user) command_args = process_mentionning_comment(body, self.mention_bot_re) for (command, arguments) in command_args: print(" :: treating", command, arguments) handler = self.actions.get(command, None) if handler: print(" :: testing who can use ", str(handler)) - if ((handler.scope == 'admin') and is_admin) or (handler.scope == 'everyone'): + if (handler.scope.value >= permission_level.value): print(" :: authorisation granted ", handler.scope) maybe_gen = handler( session=session, payload=payload, arguments=arguments) - import types if type(maybe_gen) == types.GeneratorType: gen = YieldBreaker(maybe_gen) for org_repo in gen: @@ -189,11 +191,12 @@ def dispatch_on_mention(self, body, payload, user): session_id = self.auth.idmap.get(org_repo) if session_id: target_session = self.auth.session(session_id) - if target_session.is_collaborator(torg, trepo, user): + if target_session.has_permission(torg, trepo, user, Permission.write): gen.send(target_session) else: gen.send(None) else: + print('org/repo not found', org_repo, self.auth.id_map) gen.send(None) else: print('I Cannot let you do that') diff --git a/meeseeksbox/scopes.py b/meeseeksbox/scopes.py index a5065e8..54a7377 100644 --- a/meeseeksbox/scopes.py +++ b/meeseeksbox/scopes.py @@ -2,11 +2,31 @@ Define various scopes """ +from enum import Enum + + +class Permission(Enum): + none = 0 + read = 1 + write = 2 + admin = 4 + def admin(function): - function.scope='admin' + function.scope = Permission.admin + return function + + +def read(function): + function.scope = Permission.read return function + +def write(function): + function.scope = Permission.write + return function + + def everyone(function): - function.scope='everyone' + function.scope = Permission.none return function diff --git a/meeseeksbox/utils.py b/meeseeksbox/utils.py index 3da654b..88d30f1 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -7,8 +7,10 @@ import requests import re -API_COLLABORATORS_TEMPLATE = 'https://api.github.com/repos/{org}/{repo}/collaborators/{username}' -ACCEPT_HEADER = 'application/vnd.github.machine-man-preview+json' +from .scopes import Permission + +API_COLLABORATORS_TEMPLATE = 'https://api.github.com/repos/{org}/{repo}/collaborators/{username}/permission' +ACCEPT_HEADER = 'application/vnd.github.machine-man-preview+json,application/vnd.github.korra-preview' """ Regular expression to relink issues/pr comments correctly. @@ -56,9 +58,10 @@ def __init__(self, integration_id, rsadata): # TODO: this mapping is built at startup, we should update it when we # have new / deleted installations self.idmap = {} + self._session_class = Session def session(self, installation_id): - return Session(self.integration_id, self.rsadata, installation_id) + return self._session_class(self.integration_id, self.rsadata, installation_id) def list_installations(self): """ @@ -144,22 +147,22 @@ def prepare(): response.raise_for_status() return response - def is_collaborator(self, org, repo, username): + def _get_permission(self, org, repo, username): + get_collaborators_query = API_COLLABORATORS_TEMPLATE.format( + org=org, repo=repo, username=username) + resp = self.ghrequest('GET', get_collaborators_query, None) + resp.raise_for_status() + permission = resp.json()['permission'] + print("found permission", permission , "for user ", username, "on ", org, repo) + return permission.value + + def has_permission(self, org, repo, username, level=None): """ - Check if a user is collaborator on this repository - - Right now this is a boolean, there is a new API - (application/vnd.github.korra-preview) with github which allows to get - finer grained decision. """ - get_collaborators_query = API_COLLABORATORS_TEMPLATE.format(org=org, repo=repo, username=username) - resp = self.ghrequest('GET', get_collaborators_query, None) - if resp.status_code == 204: - return True - elif resp.status_code == 404: - return False - else: - resp.raise_for_status() + if not level: + level = Permission.none + + return self._get_permission(org, repo, username) >= level.value def post_comment(self, comment_url, body): self.ghrequest('POST', comment_url, json={"body":body}) diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index c0354ee..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.5.2