summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-08-01 13:48:44 +0300
committerLars Wirzenius <liw@liw.fi>2018-08-01 15:18:54 +0300
commit036a863b00fe079e13bb1640267078ec47e6f9e5 (patch)
tree8363e515b4716375aa723f6db37f05611e806399
parentbfd3be221e51f9d140c68a40f72ce38ac3aad7ca (diff)
downloadqvisqve-036a863b00fe079e13bb1640267078ec47e6f9e5.tar.gz
Add: management API
-rw-r--r--qvisqve/__init__.py1
-rw-r--r--qvisqve/api.py5
-rw-r--r--qvisqve/entity_manager.py3
-rw-r--r--qvisqve/entity_manager_tests.py11
-rw-r--r--qvisqve/file_store.py5
-rw-r--r--qvisqve/file_store_tests.py11
-rw-r--r--qvisqve/management_router.py134
-rw-r--r--without-tests1
-rw-r--r--yarns/400-manage.yarn162
-rw-r--r--yarns/900-implements.yarn5
-rw-r--r--yarns/900-local.yarn12
11 files changed, 342 insertions, 8 deletions
diff --git a/qvisqve/__init__.py b/qvisqve/__init__.py
index ec7a48c..0627c7b 100644
--- a/qvisqve/__init__.py
+++ b/qvisqve/__init__.py
@@ -45,6 +45,7 @@ from .token import TokenGenerator
from .router import Router
from .version_router import VersionRouter
+from .management_router import ManagementRouter
from .login_router import LoginRouter
from .auth_router import AuthRouter
from .token_router import TokenRouter
diff --git a/qvisqve/api.py b/qvisqve/api.py
index 0866ae3..b63bdb6 100644
--- a/qvisqve/api.py
+++ b/qvisqve/api.py
@@ -28,8 +28,12 @@ class API:
qvisqve.log.log('info', msg_text='find_missing_route', path=path)
if self._routes is None:
+ storedir = self._config['store']
+ baseurl = self._config['token-issuer']
+
routers = [
qvisqve.VersionRouter(),
+ qvisqve.ManagementRouter(storedir, baseurl),
qvisqve.TokenRouter(
self._create_token_generator(), self._get_clients()),
qvisqve.LoginRouter(),
@@ -56,7 +60,6 @@ class API:
return tg
def _create_resource_store(self):
- qvisqve.log.log('debug', msg_text='_c_r_s 1', c=self._config)
if self._rs is None:
self._rs = qvisqve.FileStore(self._config['store'])
return self._rs
diff --git a/qvisqve/entity_manager.py b/qvisqve/entity_manager.py
index b39d7e6..a4a1705 100644
--- a/qvisqve/entity_manager.py
+++ b/qvisqve/entity_manager.py
@@ -26,6 +26,9 @@ class EntityManager:
def get(self, entity_id):
return self._store.get(self._type, entity_id)
+ def delete(self, entity_id):
+ return self._store.delete(self._type, entity_id)
+
def create(self, entity_id, entity):
self._store.create(self._type, entity_id, entity)
diff --git a/qvisqve/entity_manager_tests.py b/qvisqve/entity_manager_tests.py
index 4ad9bb1..1ca2fad 100644
--- a/qvisqve/entity_manager_tests.py
+++ b/qvisqve/entity_manager_tests.py
@@ -45,6 +45,17 @@ class EntityManagerTests(unittest.TestCase):
self.assertEqual(self.em.list(), [foo_id])
self.assertEqual(self.em.get(foo_id), entity)
+ def test_deletes_an_entity(self):
+ entity = {
+ 'foo': 'foo is cool',
+ }
+ foo_id = 'foo is my entity'
+ self.em.create(foo_id, entity)
+ self.em.delete(foo_id)
+ self.assertEqual(self.em.list(), [])
+ with self.assertRaises(qvisqve.ResourceDoesNotExist):
+ self.em.get('does-not-exist')
+
class ApplicationManagerTests(unittest.TestCase):
diff --git a/qvisqve/file_store.py b/qvisqve/file_store.py
index 3fd5186..b22f2e7 100644
--- a/qvisqve/file_store.py
+++ b/qvisqve/file_store.py
@@ -56,6 +56,11 @@ class FileStore:
with open(filename, 'w') as f:
yaml.safe_dump(resource, stream=f)
+ def delete(self, resource_type, resource_id):
+ filename = self._filename(resource_type, resource_id)
+ if os.path.exists(filename):
+ os.remove(filename)
+
class ResourceStoreError(Exception):
diff --git a/qvisqve/file_store_tests.py b/qvisqve/file_store_tests.py
index 6d27afc..9260235 100644
--- a/qvisqve/file_store_tests.py
+++ b/qvisqve/file_store_tests.py
@@ -95,3 +95,14 @@ class FileStoreTests(unittest.TestCase):
self.assertEqual(client, self.fs.get('client', client['id']))
self.assertEqual(user, self.fs.get('user', user['id']))
+
+ def test_deletes_client(self):
+ client = {
+ 'id': 'test-client',
+ }
+ self.fs.create('client', client['id'], client)
+ self.fs.delete('client', client['id'])
+ self.assertEqual(self.fs.list('client'), [])
+
+ with self.assertRaises(qvisqve.ResourceDoesNotExist):
+ self.fs.get('client', client['id'])
diff --git a/qvisqve/management_router.py b/qvisqve/management_router.py
new file mode 100644
index 0000000..7ae04b0
--- /dev/null
+++ b/qvisqve/management_router.py
@@ -0,0 +1,134 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+
+import os
+import urllib.parse
+
+
+import bottle
+
+
+import qvisqve
+import qvisqve_secrets
+
+
+class ManagementRouter(qvisqve.Router):
+
+ def __init__(self, storedir, baseurl):
+ super().__init__()
+ rs = qvisqve.FileStore(storedir)
+ self._clients = qvisqve.ClientManager(rs)
+ self._users = qvisqve.UserManager(rs)
+ self._apps = qvisqve.ApplicationManager(rs)
+ self._baseurl = baseurl
+
+ def get_routes(self):
+ clients = ManagementEndpoint(self._baseurl, '/clients', self._clients)
+ users = ManagementEndpoint(self._baseurl, '/users', self._users)
+ apps = ManagementEndpoint(self._baseurl, '/applications', self._apps)
+
+ return clients.get_routes() + users.get_routes() + apps.get_routes()
+
+
+class ManagementEndpoint:
+
+ def __init__(self, baseurl, path, entities):
+ self._baseurl = baseurl
+ self._path = path
+ self._entities = entities
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'POST',
+ 'path': self._path,
+ 'callback': self._create,
+ },
+ {
+ 'method': 'GET',
+ 'path': self._path,
+ 'callback': self._list,
+ },
+ {
+ 'method': 'GET',
+ 'path': '{}/<id>'.format(self._path),
+ 'callback': self._show,
+ },
+ {
+ 'method': 'PUT',
+ 'path': '{}/<id>/secret'.format(self._path),
+ 'callback': self._set_secret,
+ },
+ {
+ 'method': 'DELETE',
+ 'path': '{}/<id>'.format(self._path),
+ 'callback': self._delete,
+ },
+ ]
+
+ def _create(self, content_type, body, **kwargs):
+ qvisqve.log.log('info', msg_text='Creating', path=self._path)
+
+ entity_id = body.get('id')
+ if not entity_id:
+ return qvisqve.bad_request_response(
+ 'No {}'.format(self._entity_id_name))
+
+ self._entities.create(entity_id, body)
+
+ entity = self._entities.get(entity_id)
+ location = '{}{}/{}'.format(self._baseurl, self._path, entity_id)
+ return qvisqve.created_response(entity, location)
+
+ def _list(self, content_type, body, **kwargs):
+ qvisqve.log.log('info', msg_text='Listing', path=self._path)
+ # FIXME
+ return qvisqve.ok_response({"resources": []})
+
+ def _show(self, content_type, body, **kwargs):
+ qvisqve.log.log('info', msg_text='Showing client')
+
+ entity_id = kwargs['id']
+ try:
+ entity = self._entities.get(entity_id)
+ except qvisqve.ResourceDoesNotExist as e:
+ return qvisqve.not_found_response()
+
+ qvisqve.log.log('info', msg_text='Showing', entity=entity)
+ return qvisqve.ok_response(entity)
+
+ def _set_secret(self, content_type, body, **kwargs):
+ qvisqve.log.log('info', msg_text='Setting client secret')
+
+ entity_id = kwargs['id']
+ try:
+ entity = self._entities.get(entity_id)
+ except qvisqve.ResourceDoesNotExist as e:
+ return qvisqve.not_found_response()
+
+ secret = body.get('secret')
+ if not secret:
+ return qvisqve.bad_request_response('No secret')
+
+ self._entities.set_secret(entity_id, secret)
+ return qvisqve.ok_response(entity)
+
+ def _delete(self, content_type, body, **kwargs):
+ qvisqve.log.log('info', msg_text='Deleting', path=self._path)
+
+ entity_id = kwargs['id']
+ self._entities.delete(entity_id)
+ return qvisqve.ok_response('')
diff --git a/without-tests b/without-tests
index 1fc97eb..11a86db 100644
--- a/without-tests
+++ b/without-tests
@@ -7,6 +7,7 @@ qvisqve/auth_router.py
qvisqve/backend.py
qvisqve/log_setup.py
qvisqve/login_router.py
+qvisqve/management_router.py
qvisqve/responses.py
qvisqve/router.py
qvisqve/token.py
diff --git a/yarns/400-manage.yarn b/yarns/400-manage.yarn
new file mode 100644
index 0000000..6d60909
--- /dev/null
+++ b/yarns/400-manage.yarn
@@ -0,0 +1,162 @@
+Manage clients, users, applications via API
+=============================================================================
+
+ SCENARIO manage clients, users, applications
+ GIVEN an RSA key pair for token signing
+ AND a Qvisqve configuration for "https://qvisqve.example.com"
+ AND Qvisqve configuration has a token lifetime of 3600
+ AND a running Qvisqve instance
+ AND an access token for admin with scopes
+ ... uapi_clients_post
+ ... uapi_clients_get
+ ... uapi_clients_id_get
+ ... uapi_clients_id_put
+ ... uapi_clients_id_secret_put
+ ... uapi_clients_id_delete
+ ... uapi_users_post
+ ... uapi_users_get
+ ... uapi_users_id_get
+ ... uapi_users_id_put
+ ... uapi_users_id_secret_put
+ ... uapi_users_id_delete
+ ... uapi_applications_post
+ ... uapi_applications_get
+ ... uapi_applications_id_get
+ ... uapi_applications_id_put
+ ... uapi_applications_id_delete
+
+First, manage clients.
+
+ WHEN client requests GET /clients using token
+ THEN HTTP status code is 200 OK
+ AND Content-Type is application/json
+ AND JSON body matches
+ ... {
+ ... "resources": []
+ ... }
+
+ WHEN client requests POST /clients with token and body
+ ... {
+ ... "id": "james"
+ ... }
+ THEN HTTP status code is 201 Created
+ AND Location is https://qvisqve.example.com/clients/james
+
+ WHEN client requests PUT /clients/james/secret with token and body
+ ... { "secret": "hunter2" }
+ THEN HTTP status code is 200 OK
+
+ WHEN client requests GET /clients using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "resources": ["james"]
+ ... }
+
+ WHEN client requests GET /clients/james using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "id": "james"
+ ... }
+
+ WHEN client requests DELETE /clients/james with token
+ THEN HTTP status code is 200 OK
+ WHEN client requests GET /clients/james using token
+ THEN HTTP status code is 404 Not Found
+ WHEN client requests GET /clients using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "resources": []
+ ... }
+
+Then, manage users.
+
+ WHEN client requests GET /users using token
+ THEN HTTP status code is 200 OK
+ AND Content-Type is application/json
+ AND JSON body matches
+ ... {
+ ... "resources": []
+ ... }
+
+ WHEN client requests POST /users with token and body
+ ... {
+ ... "id": "sherlock"
+ ... }
+ THEN HTTP status code is 201 Created
+ AND Location is https://qvisqve.example.com/users/sherlock
+
+ WHEN client requests PUT /users/sherlock/secret with token and body
+ ... { "secret": "hunter2" }
+ THEN HTTP status code is 200 OK
+
+ WHEN client requests GET /users using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "resources": ["sherlock"]
+ ... }
+
+ WHEN client requests GET /users/sherlock using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "id": "sherlock"
+ ... }
+
+ WHEN client requests DELETE /users/sherlock with token
+ THEN HTTP status code is 200 OK
+ WHEN client requests GET /users/sherlock using token
+ THEN HTTP status code is 404 Not Found
+ WHEN client requests GET /users using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "resources": []
+ ... }
+
+Then, manage applications.
+
+ WHEN client requests GET /applications using token
+ THEN HTTP status code is 200 OK
+ AND Content-Type is application/json
+ AND JSON body matches
+ ... {
+ ... "resources": []
+ ... }
+
+ WHEN client requests POST /applications with token and body
+ ... {
+ ... "id": "MI6",
+ ... "callbacks": ["https://mi6.example.com/callback"]
+ ... }
+ THEN HTTP status code is 201 Created
+ AND Location is https://qvisqve.example.com/applications/MI6
+
+ WHEN client requests GET /applications using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "resources": ["MI6"]
+ ... }
+
+ WHEN client requests GET /applications/MI6 using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "id": "MI6",
+ ... "callbacks": ["https://mi6.example.com/callback"]
+ ... }
+
+ WHEN client requests DELETE /applications/MI6 with token
+ THEN HTTP status code is 200 OK
+ WHEN client requests GET /applications/MI6 using token
+ THEN HTTP status code is 404 Not Found
+ WHEN client requests GET /applications using token
+ THEN HTTP status code is 200 OK
+ AND JSON body matches
+ ... {
+ ... "resources": []
+ ... }
diff --git a/yarns/900-implements.yarn b/yarns/900-implements.yarn
index 7eea6d8..25a7e11 100644
--- a/yarns/900-implements.yarn
+++ b/yarns/900-implements.yarn
@@ -183,6 +183,11 @@ This chapter shows the scenario step implementations.
headers = V['headers']
assertEqual(headers['Content-Type'][:len(wanted)], wanted)
+ IMPLEMENTS THEN Location is (\S+)
+ wanted = get_next_match()
+ headers = V['headers']
+ assertEqual(headers['Location'], wanted)
+
IMPLEMENTS THEN body is a correctly signed JWT token
resp = json.loads(V['body'])
assertIn('access_token', resp)
diff --git a/yarns/900-local.yarn b/yarns/900-local.yarn
index ddce8f8..8c9fd1d 100644
--- a/yarns/900-local.yarn
+++ b/yarns/900-local.yarn
@@ -60,13 +60,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
IMPLEMENTS GIVEN an access token for (\S+) with scopes (.+)
user = get_next_match()
scopes = get_next_match()
- key = open('token.key').read()
- argv = [
- os.path.join(srcdir, 'create-token'),
- scopes,
- ]
- token = cliapp.runcmd(argv, feed_stdin=key)
- store_token(user, token)
+ key = V['privkey']
+ issuer = V['iss']
+ audience = V['aud']
+ token = create_token(key, issuer, audience, scopes)
+ V['token'] = token
## Start Qvisqve