#!/usr/bin/python3
# Copyright (C) 2018 Lars Wirzenius
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import copy
import json
import logging
import os
import sys
import urllib
import uuid
import bottle
import requests
COOKIE_NAME = 'effireg'
class EffiAPI:
_scopes = [
'create',
'update',
'show',
'delete',
'super',
]
def __init__(self, app, our_url, apiurl, client_id, client_secret):
self._add_routes(app)
self._our_url = our_url
self._apiurl = apiurl
self._auth_url = '{}/auth'.format(apiurl)
self._token_url = '{}/token'.format(apiurl)
self._client_id = client_id
self._client_secret = client_secret
self._sessions = Sessions()
def _add_routes(self, bottleapp):
routes = [
{
'method': 'GET',
'path': '/',
'callback': self._call(self._frontpage),
},
{
'method': 'GET',
'path': '/login',
'callback': self._call(self._redirect_to_login),
},
{
'method': 'GET',
'path': '/logout',
'callback': self._call(self._logout),
},
{
'method': 'GET',
'path': '/callback',
'callback': self._call(self._auth_callback),
},
]
for route in routes:
bottleapp.route(**route)
def _call(self, callback):
def helper():
r = bottle.request
logging.info('Request: path=%s', r.path)
logging.info('Request: content type: %s', r.content_type)
for h in r.headers:
logging.info('Request: headers: %s: %s', h, r.get_header(h))
logging.info('Request: body: %r', r.body.read())
r = callback()
logging.debug('Response: %r', r)
return r
return helper
def _frontpage(self):
session = None
cookie = bottle.request.get_cookie(COOKIE_NAME)
if cookie:
session = self._sessions.find_by_cookie(cookie)
if session is None:
return self._not_logged_in_page()
return self._logged_in_page(cookie)
def _not_logged_in_page(self):
return '''
You are NOT logged in.
Log in
'''
def _logged_in_page(self, cookie):
session = self._sessions.find_by_cookie(cookie)
if not session:
return self._not_logged_in_page()
members = self._get_members(session.token)
return '''
You ARE logged in. Cookie is {}.
Token is
{}
Log out
Members:
{}
'''.format(cookie, session.token, json.dumps(members, indent=4))
def _get_members(self, token):
rids = self._find_ids_of_resources_owned_by_user(token)
return self._get_resources(token, rids)
def _find_ids_of_resources_owned_by_user(self, token):
url = '{}/search'.format(self._apiurl)
cond = {
'cond': [
{
'op': '>=',
'where': 'meta',
'field': 'id',
'pattern': '',
},
]
}
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {}'.format(token),
}
logging.debug('Finding owned by user: url=%s headers=%r', url, headers)
r = requests.get(url, headers=headers, data=json.dumps(cond))
if not r.ok:
logging.error('{} {}'.format(r.status_code, r.text))
return []
obj = r.json()
return obj['resources']
def _get_resources(self, token, rids):
return [
self._get_resource(token, rid)
for rid in rids
]
def _get_resource(self, token, rid):
logging.info('Retrieving resource %s', rid)
url = '{}/mem'.format(self._apiurl)
headers = {
'Muck-Id': rid,
'Authorization': 'Bearer {}'.format(token),
}
r = requests.get(url, headers=headers)
if not r.ok:
logging.error('{} {}'.format(r.status_code, r.text))
return {}
return r.json()
def _redirect_to_frontpage(self):
headers = {
'Location': self._our_url,
}
logging.info('Redirecting with headers %s', headers)
return bottle.HTTPResponse(status=302 , headers=headers)
def _redirect_to_login(self):
session = self._sessions.new()
params = {
'response_type': 'code',
'scope': ' '.join(self._scopes),
'client_id': self._client_id,
'state': session.state,
'redirect_uri': '{}/callback'.format(self._our_url),
}
auth_url = '{}?{}'.format(
self._auth_url, urllib.parse.urlencode(params))
headers = {
'Location': auth_url,
}
logging.info('Redirecting with headers %s', headers)
return bottle.HTTPResponse(status=302 , headers=headers)
def _auth_callback(self):
code = bottle.request.query.get('code')
state = bottle.request.query.get('state')
if code is None or state is None:
return regesta_api.bad_request_response()
token = self._get_token_using_authz_code(code)
session = self._sessions.find_by_state(state)
if session is None:
return self._redirect_to_login()
session.token = token
logging.debug('Redirecting to / with cookie %s', session.cookie)
bottle.response.set_cookie(COOKIE_NAME, session.cookie, path='/')
bottle.redirect(self._our_url)
return None
headers = {
'Location': self._our_url,
}
r = bottle.HTTPResponse(302, headers=headers)
r.set_cookie(COOKIE_NAME, session.cookie, path='/')
logging.debug('Redirecting to / with cookie %s', session.cookie)
return r
def _get_token_using_authz_code(self, code):
auth = (self._client_id, self._client_secret)
data = {
'grant_type': 'authorization_code',
'code': code,
}
url = self._token_url
logging.debug('Requesting token using authz code from %s', url)
r = requests.post(url, auth=auth, data=data)
if not r.ok:
logging.error(
'Could not get token using authz code: %s %s',
r.status_code, r.text)
raise bottle.HTTPError(500)
obj = r.json()
return obj[u'access_token']
def _logout(self):
cookie = bottle.request.get_cookie(COOKIE_NAME)
if not cookie:
return self._redirect_to_frontpage()
session = self._sessions.find_by_cookie(cookie)
self._sessions.delete(session)
logging.debug('Redirecting to / without')
bottle.response.delete_cookie(COOKIE_NAME, path='/')
bottle.redirect(self._our_url)
return None
class Session:
def __init__(self):
self.id = str(uuid.uuid4())
self.state = str(uuid.uuid4())
self.cookie = str(uuid.uuid4())
self.token = None
class Sessions:
def __init__(self):
self._sessions = []
def new(self):
session = Session()
self._sessions.append(session)
return session
def delete(self, session):
self._sessions = [s for s in self._sessions if s.id != session.id]
def find_by_state(self, state):
return self._find(lambda s: s.state == state)
def find_by_cookie(self, cookie):
return self._find(lambda s: s.cookie == cookie)
def _find(self, is_this_it):
for session in self._sessions:
if is_this_it(session):
return session
return None
def main():
with open(sys.argv[1]) as f:
config = json.load(f)
logging.basicConfig(
filename=config['log'], level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(message)s')
logging.info('Effi-reg starts')
if config.get('pid'):
pid = os.getpid()
with open(config['pid'], 'w') as f:
f.write(str(pid))
app = bottle.default_app()
api = EffiAPI(
app, config['our-url'], config['api-url'],
config['client-id'], config['client-secret'])
app.run(host='127.0.0.1', port=8181)
if __name__ == '__main__':
main()