summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-07-31 11:14:13 +0300
committerLars Wirzenius <liw@liw.fi>2018-07-31 15:54:02 +0300
commit9ead1c5c91e3c75274aa56dca2b17036cdc45573 (patch)
treec130d2b0b5a215f88b3d03cd876d9e9ef1648041
parent329bb31dbfb7675a21605d552999c59432ef7b10 (diff)
downloadqvisqve-9ead1c5c91e3c75274aa56dca2b17036cdc45573.tar.gz
Add: FileStore, managers for users, clients, applications
Add: UserManager
-rw-r--r--qvisqve/__init__.py20
-rw-r--r--qvisqve/api.py22
-rw-r--r--qvisqve/app.py3
-rw-r--r--qvisqve/auth_router.py4
-rw-r--r--qvisqve/authn_entity_manager.py80
-rw-r--r--qvisqve/authn_entity_manager_tests.py113
-rw-r--r--qvisqve/entity_manager.py50
-rw-r--r--qvisqve/entity_manager_tests.py69
-rw-r--r--qvisqve/file_store.py70
-rw-r--r--qvisqve/file_store_tests.py97
-rw-r--r--qvisqve/token_router.py12
-rw-r--r--yarns/200-client-creds.yarn35
-rw-r--r--yarns/lib.py30
13 files changed, 573 insertions, 32 deletions
diff --git a/qvisqve/__init__.py b/qvisqve/__init__.py
index 32032b6..7d2b68f 100644
--- a/qvisqve/__init__.py
+++ b/qvisqve/__init__.py
@@ -14,6 +14,25 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .version import __version__, __version_info__
+from .log_setup import setup_logging, log
+
+from .file_store import (
+ FileStore,
+ ResourceStoreError,
+ ResourceDoesNotExist,
+)
+
+from .entity_manager import (
+ EntityManager,
+ ApplicationManager,
+)
+
+from .authn_entity_manager import (
+ AuthenticatingEntityManager,
+ ClientManager,
+ UserManager,
+)
+
from .responses import (
bad_request_response,
created_response,
@@ -21,7 +40,6 @@ from .responses import (
unauthorized_response,
found_response,
)
-from .log_setup import setup_logging, log
from .token import TokenGenerator
from .router import Router
diff --git a/qvisqve/api.py b/qvisqve/api.py
index 58f09c3..6c3fe34 100644
--- a/qvisqve/api.py
+++ b/qvisqve/api.py
@@ -21,22 +21,25 @@ class API:
def __init__(self, config):
self._config = config
+ self._rs = None
def find_missing_route(self, path):
qvisqve.log.log('info', msg_text='find_missing_route', path=path)
routers = [
qvisqve.VersionRouter(),
- qvisqve.LoginRouter(),
- qvisqve.AuthRouter(self._config.get('applications', {})),
qvisqve.TokenRouter(
self._create_token_generator(), self._get_clients()),
+ qvisqve.LoginRouter(),
+ qvisqve.AuthRouter(self._get_applications()),
]
routes = []
for router in routers:
routes.extend(router.get_routes())
+ qvisqve.log.log(
+ 'debug', msg_text='missing routes created', routes=routes)
return routes
def _create_token_generator(self):
@@ -47,5 +50,18 @@ class API:
tg.set_signing_key(cfg['token-private-key'])
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
+
def _get_clients(self):
- return self._config.get('clients', {})
+ rs = self._create_resource_store()
+ cm = qvisqve.ClientManager(rs)
+ return cm
+
+ def _get_applications(self):
+ rs = self._create_resource_store()
+ am = qvisqve.ApplicationManager(rs)
+ return am
diff --git a/qvisqve/app.py b/qvisqve/app.py
index a1f1910..517c326 100644
--- a/qvisqve/app.py
+++ b/qvisqve/app.py
@@ -57,8 +57,7 @@ default_config = {
'token-public-key': None,
'token-private-key': None,
'token-lifetime': None,
- 'clients': None,
- 'applications': None,
+ 'store': None,
}
diff --git a/qvisqve/auth_router.py b/qvisqve/auth_router.py
index 4e4bec1..717e46f 100644
--- a/qvisqve/auth_router.py
+++ b/qvisqve/auth_router.py
@@ -49,7 +49,9 @@ class AuthRouter(qvisqve.Router):
# - create and store auth code
# - use callback url provided in request
- callback_url = self._apps.get('facade') # FIXME get real app name
+ # FIXME use real app name here
+ callbacks = self._apps.get_callbacks('facade')
+ callback_url = callbacks[0]
params = urllib.parse.urlencode({'code': 123})
url = '{}?{}'.format(callback_url, params)
diff --git a/qvisqve/authn_entity_manager.py b/qvisqve/authn_entity_manager.py
new file mode 100644
index 0000000..32e2da3
--- /dev/null
+++ b/qvisqve/authn_entity_manager.py
@@ -0,0 +1,80 @@
+# 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 qvisqve
+import qvisqve_secrets
+
+
+class AuthenticatingEntityManager(qvisqve.EntityManager):
+
+ _hashed = 'hashed_secret'
+
+ def set_secret(self, entity_id, cleartext_secret):
+ entity = self.get(entity_id)
+ hasher = qvisqve_secrets.SecretHasher()
+ entity[self._hashed] = hasher.hash(cleartext_secret)
+ self.create(entity_id, entity)
+
+ def is_valid_secret(self, entity_id, cleartext_secret):
+ try:
+ entity = self.get(entity_id)
+ except qvisqve.ResourceDoesNotExist:
+ qvisqve.log.log(
+ 'error', msg_text='Entity does not exist',
+ entity_id=entity_id)
+ return False
+
+ hashed_secret = entity.get(self._hashed)
+ if not hashed_secret:
+ qvisqve.log.log(
+ 'error', msg_text='Entity does not have a hashed secret',
+ entity_id=entity_id)
+ return False
+
+ hasher = qvisqve_secrets.SecretHasher()
+ if not hasher.is_correct(hashed_secret, cleartext_secret):
+ qvisqve.log.log(
+ 'error', msg_text='Client-supplied secret is WRONG',
+ entity_id=entity_id)
+ return False
+
+ qvisqve.log.log(
+ 'debug', msg_text='Client-supplied secret IS correct',
+ entity_id=entity_id)
+ return True
+
+
+class ClientManager(AuthenticatingEntityManager):
+
+ def __init__(self, rs):
+ super().__init__(rs, 'client')
+
+ def set_allowed_scopes(self, client_id, scopes):
+ client = self.get(client_id)
+ client['allowed_scopes'] = scopes
+ self.create(client_id, client)
+
+ def get_allowed_scopes(self, client_id):
+ client = self.get(client_id)
+ return client.get('allowed_scopes', [])
+
+
+class UserManager(AuthenticatingEntityManager):
+
+ def __init__(self, rs):
+ super().__init__(rs, 'user')
diff --git a/qvisqve/authn_entity_manager_tests.py b/qvisqve/authn_entity_manager_tests.py
new file mode 100644
index 0000000..d46cd03
--- /dev/null
+++ b/qvisqve/authn_entity_manager_tests.py
@@ -0,0 +1,113 @@
+# 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 shutil
+import tempfile
+import unittest
+
+import qvisqve
+
+
+class AuthenticatingEntityManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ fs = qvisqve.FileStore(self.tempdir)
+ self.aem = qvisqve.AuthenticatingEntityManager(fs, 'client')
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_does_not_validate_secret_if_entity_does_not_exist(self):
+ self.assertFalse(
+ self.aem.is_valid_secret('does-not-exist', 'whatever'))
+
+ def test_does_not_validate_secret_if_not_stored(self):
+ secret = 'hunter2'
+ client = {
+ 'id': 'test-client',
+ }
+
+ self.aem.create(client['id'], client)
+ self.assertFalse(self.aem.is_valid_secret(client['id'], secret))
+
+ def test_validates_secret(self):
+ secret = 'hunter2'
+ client = {
+ 'id': 'test-client',
+ }
+
+ self.aem.create(client['id'], client)
+ self.aem.set_secret(client['id'], secret)
+ self.assertFalse(self.aem.is_valid_secret(client['id'], 'invalid'))
+ self.assertTrue(self.aem.is_valid_secret(client['id'], secret))
+
+
+class ClientManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ fs = qvisqve.FileStore(self.tempdir)
+ self.cm = qvisqve.ClientManager(fs)
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_validates_client_secret(self):
+ secret = 'hunter2'
+ client = {
+ 'id': 'test-client',
+ }
+
+ self.cm.create(client['id'], client)
+ self.cm.set_secret(client['id'], secret)
+ self.assertTrue(self.cm.is_valid_secret(client['id'], secret))
+
+ def test_returns_empty_list_of_scopes_initially(self):
+ client = {
+ 'id': 'test-client',
+ }
+
+ self.cm.create(client['id'], client)
+ self.assertEqual(self.cm.get_allowed_scopes(client['id']), [])
+
+ def test_sets_allowed_scopes(self):
+ client = {
+ 'id': 'test-client',
+ }
+ scopes = ['foo', 'bar']
+
+ self.cm.create(client['id'], client)
+ self.cm.set_allowed_scopes(client['id'], scopes)
+ self.assertEqual(self.cm.get_allowed_scopes(client['id']), scopes)
+
+
+class UserManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ fs = qvisqve.FileStore(self.tempdir)
+ self.um = qvisqve.UserManager(fs)
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_creates_user(self):
+ user = {
+ 'id': 'tomjon',
+ }
+ self.um.create(user['id'], user)
+ self.assertEqual(self.um.get(user['id']), user)
diff --git a/qvisqve/entity_manager.py b/qvisqve/entity_manager.py
new file mode 100644
index 0000000..da42f5b
--- /dev/null
+++ b/qvisqve/entity_manager.py
@@ -0,0 +1,50 @@
+# 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 yaml
+
+
+class EntityManager:
+
+ def __init__(self, rs, resource_type):
+ self._store = rs
+ self._type = resource_type
+
+ def list(self):
+ return self._store.list(self._type)
+
+ def get(self, entity_id):
+ return self._store.get(self._type, entity_id)
+
+ def create(self, entity_id, entity):
+ self._store.create(self._type, entity_id, entity)
+
+
+class ApplicationManager(EntityManager):
+
+ def __init__(self, rs):
+ super().__init__(rs, 'application')
+
+ def get_callbacks(self, app_id):
+ app = self.get(app_id)
+ return app.get('callbacks', [])
+
+ def add_callback(self, app_id, url):
+ app = self.get(app_id)
+ app['callbacks'] = app.get('callbacks', []) + [url]
+ self.create(app_id, app)
diff --git a/qvisqve/entity_manager_tests.py b/qvisqve/entity_manager_tests.py
new file mode 100644
index 0000000..5088da4
--- /dev/null
+++ b/qvisqve/entity_manager_tests.py
@@ -0,0 +1,69 @@
+# 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 shutil
+import tempfile
+import unittest
+
+import qvisqve
+
+
+class EntityManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ fs = qvisqve.FileStore(self.tempdir)
+ self.em = qvisqve.EntityManager(fs, 'foo')
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_has_no_entities_initially(self):
+ self.assertEqual(self.em.list(), [])
+ with self.assertRaises(qvisqve.ResourceDoesNotExist):
+ self.em.get('does-not-exist')
+
+ def test_creates_an_entity(self):
+ foo = {
+ 'foo': 'foo is cool',
+ }
+ foo_id = 'foo is my entity'
+ self.em.create(foo_id, foo)
+ self.assertEqual(self.em.list(), [foo_id])
+ self.assertEqual(self.em.get(foo_id), foo)
+
+
+class ApplicationManagerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ fs = qvisqve.FileStore(self.tempdir)
+ self.am = qvisqve.ApplicationManager(fs)
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_returns_no_callbacks_initially(self):
+ appid = 'test-app'
+ self.am.create(appid, {})
+ self.assertEqual(self.am.get_callbacks(appid), [])
+
+ def test_sets_callbacks(self):
+ appid = 'test-app'
+ url = 'http://facade.example.com/callback'
+ self.am.create(appid, {})
+ self.am.add_callback(appid, url)
+ self.assertEqual(self.am.get_callbacks(appid), [url])
diff --git a/qvisqve/file_store.py b/qvisqve/file_store.py
new file mode 100644
index 0000000..3fd5186
--- /dev/null
+++ b/qvisqve/file_store.py
@@ -0,0 +1,70 @@
+# 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 yaml
+
+
+class FileStore:
+
+ def __init__(self, dirname):
+ self._dirname = dirname
+
+ def _typedir(self, resource_type):
+ return os.path.join(self._dirname, resource_type)
+
+ def _filename(self, resource_type, resource_id):
+ dirname = self._typedir(resource_type)
+ return os.path.join(dirname, resource_id)
+
+ def list(self, resource_type):
+ dirname = self._typedir(resource_type)
+ if not os.path.isdir(dirname):
+ return []
+ return [
+ x
+ for x in sorted(os.listdir(self._typedir(resource_type)))
+ ]
+
+ def get(self, resource_type, resource_id):
+ filename = self._filename(resource_type, resource_id)
+ if os.path.exists(filename):
+ with open(filename) as f:
+ return yaml.safe_load(f)
+ raise ResourceDoesNotExist(resource_type, resource_id)
+
+ def create(self, resource_type, resource_id, resource):
+ dirname = self._typedir(resource_type)
+ if not os.path.exists(dirname):
+ os.mkdir(dirname)
+
+ filename = self._filename(resource_type, resource_id)
+ with open(filename, 'w') as f:
+ yaml.safe_dump(resource, stream=f)
+
+
+class ResourceStoreError(Exception):
+
+ pass
+
+
+class ResourceDoesNotExist(ResourceStoreError):
+
+ def __init__(self, resource_type, resource_id):
+ super().__init__(
+ 'Resource does not exist: {}:{}'.format(
+ resource_type, resource_id))
diff --git a/qvisqve/file_store_tests.py b/qvisqve/file_store_tests.py
new file mode 100644
index 0000000..6d27afc
--- /dev/null
+++ b/qvisqve/file_store_tests.py
@@ -0,0 +1,97 @@
+# 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 shutil
+import tempfile
+import unittest
+
+import qvisqve
+
+
+class FileStoreTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ self.fs = qvisqve.FileStore(self.tempdir)
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_has_no_clients_initially(self):
+ self.assertEqual(self.fs.list('client'), [])
+
+ def test_has_no_users_initially(self):
+ self.assertEqual(self.fs.list('user'), [])
+
+ def test_raises_error_getting_nonexistent_resource(self):
+ with self.assertRaises(qvisqve.ResourceDoesNotExist):
+ self.fs.get('client', 'does-not-exist')
+
+ def test_creates_client(self):
+ client = {
+ 'id': 'test-client',
+ }
+ self.fs.create('client', client['id'], client)
+ self.assertEqual(self.fs.list('client'), [client['id']])
+
+ gotten = self.fs.get('client', client['id'])
+ self.assertEqual(client, gotten)
+
+ def test_updates_client(self):
+ client = {
+ 'id': 'test-client',
+ }
+ self.fs.create('client', client['id'], client)
+
+ updated = dict(client)
+ updated['foo'] = 'bar'
+ self.fs.create('client', client['id'], updated)
+ self.assertEqual(self.fs.get('client', client['id']), updated)
+
+ def test_two_clients(self):
+ one = {
+ 'id': 'first-client',
+ }
+ self.fs.create('client', one['id'], one)
+ self.assertEqual(self.fs.list('client'), [one['id']])
+
+ two = {
+ 'id': 'second-client',
+ }
+ self.fs.create('client', two['id'], two)
+ self.assertEqual(self.fs.list('client'), [one['id'], two['id']])
+
+ gotten = self.fs.get('client', two['id'])
+ self.assertEqual(two, gotten)
+
+ def test_creates_client_and_user(self):
+ client = {
+ 'id': 'test',
+ 'type': 'client',
+ }
+
+ user = {
+ 'id': 'test',
+ 'type': 'user',
+ }
+
+ self.fs.create('client', client['id'], client)
+ self.fs.create('user', user['id'], user)
+ self.assertEqual(self.fs.list('client'), [client['id']])
+ self.assertEqual(self.fs.list('user'), [user['id']])
+
+ self.assertEqual(client, self.fs.get('client', client['id']))
+ self.assertEqual(user, self.fs.get('user', user['id']))
diff --git a/qvisqve/token_router.py b/qvisqve/token_router.py
index dd01587..911e899 100644
--- a/qvisqve/token_router.py
+++ b/qvisqve/token_router.py
@@ -27,12 +27,14 @@ import qvisqve_secrets
class TokenRouter(qvisqve.Router):
def __init__(self, token_generator, clients):
+ qvisqve.log.log('debug', msg_text='TokenRouter init starts')
super().__init__()
- args = (Clients(clients), token_generator)
+ args = (clients, token_generator)
self._grants = {
'client_credentials': ClientCredentialsGrant(*args),
'authorization_code': AuthorizationCodeGrant(*args),
}
+ qvisqve.log.log('debug', msg_text='TokenRouter created')
def get_routes(self):
return [
@@ -81,8 +83,14 @@ class Grant:
class ClientCredentialsGrant(Grant):
def get_token(self, request, params):
+ qvisqve.log.log(
+ 'debug', msg_text='ClientCredentialGrant.get_token called',
+ request=request, params=params)
+
client_id, client_secret = request.auth
- if not self._clients.is_correct_secret(client_id, client_secret):
+ if not self._clients.is_valid_secret(client_id, client_secret):
+ qvisqve.log.log(
+ 'error', msg_text='Client token request is unauthorized')
return qvisqve.unauthorized_response('Unauthorized')
scope = self._get_scope(params)
diff --git a/yarns/200-client-creds.yarn b/yarns/200-client-creds.yarn
index f251c71..9eff22a 100644
--- a/yarns/200-client-creds.yarn
+++ b/yarns/200-client-creds.yarn
@@ -21,8 +21,8 @@ The `USERPASS` has the client id and secret encoded as is usual for
[HTTP Basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication
Qvisqve checks the `grant_type` parameter, and extracts `USERPASS` to
-get the client id and secret. It compares them against a static list
-of clients, which it reads at startup from its configuration file:
+get the client id and secret. It compares them against statically
+created clients, which it reads from the filesystem.
EXAMPLE Qvisqve configuration file in YAML
config:
@@ -34,19 +34,24 @@ of clients, which it reads at startup from its configuration file:
... deleted from example
LkLFQC7Y66OYjna457hU545hfF99j7nxdseXQEhV96E4RUIub+6vS8TYDEk=
-----END RSA PRIVATE KEY-----
- clients:
- test_api:
- client_secret:
- N: 16384
- hash: 5cf3b9cab1eacc818b73d229db...a023e938ee598f6c49749ef0429a889f7
- key_len: 128
- p: 1
- r: 8
- salt: 18112c4c50993ca5db908a15519c51e1
- version: 1
- allowed_scopes:
- - foo
- - bar
+ store: /var/lib/qvisqve
+
+Each client will be stored as a separate YAML file under the directory
+configured in the "store" configuration variable. For example, the
+client `test_api` is stored in `/var/lib/qvisqve/clients/test_api`:
+
+ EXAMPLE
+ client_secret:
+ N: 16384
+ hash: 5cf3b9cab1eacc818b73d229db...a023e938ee598f6c49749ef0429a889f7
+ key_len: 128
+ p: 1
+ r: 8
+ salt: 18112c4c50993ca5db908a15519c51e1
+ version: 1
+ allowed_scopes:
+ - foo
+ - bar
Qvisqve checks that the client id given by the client is found, and
that the offered client secret matches what's in the configuration
diff --git a/yarns/lib.py b/yarns/lib.py
index 9ed7f59..56707ba 100644
--- a/yarns/lib.py
+++ b/yarns/lib.py
@@ -177,15 +177,30 @@ def start_qvisqve():
V['port'] = cliapp.runcmd([os.path.join(srcdir, 'randport' )]).strip()
V['API_URL'] = 'http://127.0.0.1:{}'.format(V['port'])
- clients = {}
+ store = os.path.join(datadir, 'store')
+ os.mkdir(store)
+ os.mkdir(os.path.join(store, 'client'))
+ os.mkdir(os.path.join(store, 'application'))
+
if V['client_id'] and V['client_secret']:
sh = qvisqve_secrets.SecretHasher()
- clients = {
- V['client_id']: {
- 'client_secret': sh.hash(V['client_secret']),
- 'allowed_scopes': V['allowed_scopes'],
- },
+ client = {
+ 'hashed_secret': sh.hash(V['client_secret']),
+ 'allowed_scopes': V['allowed_scopes'],
+ }
+
+ filename = os.path.join(store, 'client', V['client_id'])
+ with open(filename, 'w') as f:
+ yaml.safe_dump(client, stream=f)
+
+ apps = V['applications']
+ for name in apps or []:
+ filename = os.path.join(store, 'application', name)
+ spec = {
+ 'callbacks': [apps[name]],
}
+ with open(filename, 'w') as f:
+ yaml.safe_dump(spec, stream=f)
config = {
'gunicorn': 'background',
@@ -201,8 +216,7 @@ def start_qvisqve():
'token-public-key': V['pubkey'],
'token-issuer': V['iss'],
'token-lifetime': 3600,
- 'clients': clients,
- 'applications': V['applications'] or {},
+ 'store': store,
}
env = dict(os.environ)
env['QVISQVE_CONFIG'] = os.path.join(datadir, 'qvisqve.yaml')