From 1b6d0dfc1cf1d31d305c8218acfe101b44927237 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 26 Dec 2016 21:56:33 +0100 Subject: [PATCH 01/11] 'nuke main' --- Procfile | 1 - meeseeksbox/__init__.py | 21 +++------------------ meeseeksbox/__main__.py | 2 -- runtime.txt | 1 - 4 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 Procfile delete mode 100644 meeseeksbox/__main__.py delete mode 100644 runtime.txt 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/meeseeksbox/__init__.py b/meeseeksbox/__init__.py index 6614e55..4b6f630 100644 --- a/meeseeksbox/__init__.py +++ b/meeseeksbox/__init__.py @@ -1,6 +1,9 @@ import os import base64 from .core import Config +from .core import MeeseeksBox + +__version__ = '0.0.1' def load_config_from_env(): """ @@ -30,21 +33,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/runtime.txt b/runtime.txt deleted file mode 100644 index c0354ee..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.5.2 From aa623243fb49fd7e74d6eb08f3b2ea7aa24bbcf9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 26 Dec 2016 22:00:04 +0100 Subject: [PATCH 02/11] Prepare for the first release. --- flit.ini | 11 +++++++++++ meeseeksbox/__init__.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) 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 4b6f630..8d78269 100644 --- a/meeseeksbox/__init__.py +++ b/meeseeksbox/__init__.py @@ -1,9 +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 from .core import MeeseeksBox -__version__ = '0.0.1' +version_info = (0,0,1) + +__version__ = '.'.join(map(str,version_info)) def load_config_from_env(): """ From a3dc560a1412cc31d457ab7687fc0a80b4bb7afd Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 27 Dec 2016 12:44:12 +0100 Subject: [PATCH 03/11] 'debug' --- meeseeksbox/commands.py | 3 +++ meeseeksbox/core.py | 1 + 2 files changed, 4 insertions(+) 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..c7320d6 100644 --- a/meeseeksbox/core.py +++ b/meeseeksbox/core.py @@ -194,6 +194,7 @@ def dispatch_on_mention(self, body, payload, user): 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') From 00f660d8fcd318aa633cd16c7013cb80fd7a0eef Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 27 Dec 2016 12:45:41 +0100 Subject: [PATCH 04/11] 'debug version 0.0.2' --- meeseeksbox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meeseeksbox/__init__.py b/meeseeksbox/__init__.py index 8d78269..819d928 100644 --- a/meeseeksbox/__init__.py +++ b/meeseeksbox/__init__.py @@ -12,7 +12,7 @@ from .core import Config from .core import MeeseeksBox -version_info = (0,0,1) +version_info = (0,0,2) __version__ = '.'.join(map(str,version_info)) From d8f0c64330397e6b9a63b8abfddb2b80bb9d1f76 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 5 Jan 2017 15:53:05 +0100 Subject: [PATCH 05/11] Typos and log value of GitHub Event. --- meeseeksbox/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/meeseeksbox/core.py b/meeseeksbox/core.py index c7320d6..084853e 100644 --- a/meeseeksbox/core.py +++ b/meeseeksbox/core.py @@ -85,7 +85,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 +96,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 +115,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): From a3e1c48cfb90b903f24553906a87775068f8f3f8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 5 Jan 2017 16:11:11 +0100 Subject: [PATCH 06/11] Implement finer grained permission checking --- meeseeksbox/utils.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/meeseeksbox/utils.py b/meeseeksbox/utils.py index 3da654b..fd23b94 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -7,8 +7,8 @@ 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' +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. @@ -154,12 +154,9 @@ def is_collaborator(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) - if resp.status_code == 204: - return True - elif resp.status_code == 404: - return False - else: - resp.raise_for_status() + resp.raise_for_status() + return resp.json()['permission'] in ('admin', 'write') + def post_comment(self, comment_url, body): self.ghrequest('POST', comment_url, json={"body":body}) From cc505211d04bcaead906b2bb3212c0624fe04ac2 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 5 Jan 2017 16:21:19 +0100 Subject: [PATCH 07/11] Update framework to use new korra API and allow custom Session Object --- meeseeksbox/__init__.py | 2 +- meeseeksbox/utils.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/meeseeksbox/__init__.py b/meeseeksbox/__init__.py index 819d928..d031049 100644 --- a/meeseeksbox/__init__.py +++ b/meeseeksbox/__init__.py @@ -12,7 +12,7 @@ from .core import Config from .core import MeeseeksBox -version_info = (0,0,2) +version_info = (0, 0, 3) __version__ = '.'.join(map(str,version_info)) diff --git a/meeseeksbox/utils.py b/meeseeksbox/utils.py index fd23b94..d6d33af 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -56,9 +56,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): """ From 45efd46d5d954e57b4856fa677ab7e7e883f65b3 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 5 Jan 2017 17:40:01 +0100 Subject: [PATCH 08/11] Refactor permission level --- meeseeksbox/core.py | 9 +++++---- meeseeksbox/scopes.py | 24 ++++++++++++++++++++++-- meeseeksbox/utils.py | 16 ++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/meeseeksbox/core.py b/meeseeksbox/core.py index 084853e..ff480f8 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 .scope import Permission from yieldbreaker import YieldBreaker @@ -171,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: @@ -190,7 +191,7 @@ 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) 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 d6d33af..788bb3f 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -7,6 +7,8 @@ import requests import re +from .scope 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' @@ -158,6 +160,20 @@ def is_collaborator(self, org, repo, username): resp.raise_for_status() return resp.json()['permission'] in ('admin', 'write') + 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() + return resp.json()['permission'].value + + def has_permission(self, org, repo, username, level=None): + """ + """ + 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}) From 020bec43b59a321f84930758a4c9655ddd8caea2 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 6 Jan 2017 13:23:28 +0100 Subject: [PATCH 09/11] 'remove is_collaborator in favor of has_permission' --- meeseeksbox/utils.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/meeseeksbox/utils.py b/meeseeksbox/utils.py index 788bb3f..6678d52 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -147,19 +147,6 @@ def prepare(): response.raise_for_status() return response - def is_collaborator(self, org, repo, username): - """ - 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) - resp.raise_for_status() - return resp.json()['permission'] in ('admin', 'write') - def _get_permission(self, org, repo, username): get_collaborators_query = API_COLLABORATORS_TEMPLATE.format( org=org, repo=repo, username=username) From c53fda587569ab189123fe6b1071cbccf99480c8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 6 Jan 2017 13:25:09 +0100 Subject: [PATCH 10/11] 'log permission' --- meeseeksbox/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meeseeksbox/utils.py b/meeseeksbox/utils.py index 6678d52..0fcd8bf 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -152,7 +152,9 @@ def _get_permission(self, org, repo, username): org=org, repo=repo, username=username) resp = self.ghrequest('GET', get_collaborators_query, None) resp.raise_for_status() - return resp.json()['permission'].value + permission = resp.json()['permission'] + print("found permission", permission , "for user ", user, "on ", org, repo) + return permission.value def has_permission(self, org, repo, username, level=None): """ From 5ae1facdb4aa8835863f8291871e3a4f7a9288bc Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 6 Jan 2017 13:25:49 +0100 Subject: [PATCH 11/11] bump to 0.0.4 --- meeseeksbox/__init__.py | 3 +-- meeseeksbox/core.py | 2 +- meeseeksbox/utils.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/meeseeksbox/__init__.py b/meeseeksbox/__init__.py index d031049..56518f1 100644 --- a/meeseeksbox/__init__.py +++ b/meeseeksbox/__init__.py @@ -10,9 +10,8 @@ import os import base64 from .core import Config -from .core import MeeseeksBox -version_info = (0, 0, 3) +version_info = (0, 0, 4) __version__ = '.'.join(map(str,version_info)) diff --git a/meeseeksbox/core.py b/meeseeksbox/core.py index ff480f8..4d19322 100644 --- a/meeseeksbox/core.py +++ b/meeseeksbox/core.py @@ -7,7 +7,7 @@ import tornado.ioloop from .utils import Authenticator -from .scope import Permission +from .scopes import Permission from yieldbreaker import YieldBreaker diff --git a/meeseeksbox/utils.py b/meeseeksbox/utils.py index 0fcd8bf..88d30f1 100644 --- a/meeseeksbox/utils.py +++ b/meeseeksbox/utils.py @@ -7,7 +7,7 @@ import requests import re -from .scope import Permission +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' @@ -153,7 +153,7 @@ def _get_permission(self, org, repo, username): resp = self.ghrequest('GET', get_collaborators_query, None) resp.raise_for_status() permission = resp.json()['permission'] - print("found permission", permission , "for user ", user, "on ", org, repo) + print("found permission", permission , "for user ", username, "on ", org, repo) return permission.value def has_permission(self, org, repo, username, level=None):