diff options
author | Lars Wirzenius <liw@liw.fi> | 2018-11-16 21:36:04 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2018-11-16 21:36:04 +0200 |
commit | 6d621d3a51ba68f0a436d5c5b27ace6cb2825f50 (patch) | |
tree | 6817d537d5475ccd3b8d888e20cdc51bd357f33e | |
parent | d30c7d5dac5891ad86a3491e198cb384e466932e (diff) | |
download | muck-poc-6d621d3a51ba68f0a436d5c5b27ace6cb2825f50.tar.gz |
Change: allow super users to impersonate other users
-rw-r--r-- | muck/request.py | 6 | ||||
-rw-r--r-- | muck/request_tests.py | 11 | ||||
-rwxr-xr-x | muck_poc | 18 | ||||
-rw-r--r-- | yarns/200-super.yarn | 43 | ||||
-rw-r--r-- | yarns/900-implements.yarn | 15 | ||||
-rw-r--r-- | yarns/lib.py | 3 |
6 files changed, 95 insertions, 1 deletions
diff --git a/muck/request.py b/muck/request.py index f6e406e..66795ff 100644 --- a/muck/request.py +++ b/muck/request.py @@ -28,3 +28,9 @@ class Request: def get_authorization(self): return self._headers.get('Authorization') + + def get_user(self): + user = self._headers.get('Muck-User') + if user: + user = user.strip() + return user diff --git a/muck/request_tests.py b/muck/request_tests.py index 7de2393..eb6d0c4 100644 --- a/muck/request_tests.py +++ b/muck/request_tests.py @@ -34,3 +34,14 @@ class RequestTests(unittest.TestCase): 'Authorization': 'Bearer XXX', }) self.assertEqual(r.get_authorization(), 'Bearer XXX') + + def test_does_not_specify_user_by_default(self): + r = muck.Request(method='GET') + self.assertEqual(r.get_user(), None) + + def test_specifies_user_in_header(self): + r = muck.Request(method='GET') + r.add_headers({ + 'Muck-User': 'tomjon', + }) + self.assertEqual(r.get_user(), 'tomjon') @@ -14,6 +14,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +import copy import json import logging import os @@ -83,6 +84,7 @@ class MuckAPI: r.add_headers(rr.headers) if self._ac.request_is_allowed(r, req_method, [req_scope]): claims = self._ac.get_claims_from_token(r) + claims = self._claims_as_effective_user(r, claims) return callback(claims) logging.error('Access denied') return bottle.HTTPError(401) @@ -187,7 +189,8 @@ class MuckAPI: return ms[rid] def _access_is_allowed(self, meta, claims): - return claims['sub'] == meta['owner'] + scopes = claims.get('scope', '').split() + return claims['sub'] == meta['owner'] or 'super' in scopes def _create_response(self, status, operation, meta, res): headers = self._meta_headers(meta) @@ -201,6 +204,19 @@ class MuckAPI: 'Muck-Owner': meta['owner'], } + def _claims_as_effective_user(self, r, claims): + scopes = claims.get('scope', '').split() + if 'super' in scopes: + claims = copy.deepcopy(claims) + user = r.get_user() + if user: + claims['sub'] = user + logging.info( + 'Pretending to be %s (claims: %r)', claims['sub'], claims) + else: + logging.info('Reuqest by normal user') + return claims + with open(sys.argv[1]) as f: config = json.load(f) diff --git a/yarns/200-super.yarn b/yarns/200-super.yarn new file mode 100644 index 0000000..d07e451 --- /dev/null +++ b/yarns/200-super.yarn @@ -0,0 +1,43 @@ +# A happy path scenario + +This scenario does some basic resource management via the Muck API. + + SCENARIO super user + +Start Muck. This also sets up access to it for the user by getting an +access token, which will be used for all requests. + + GIVEN a running Muck + + GIVEN a user tomjon with superuser access + +Create a simple resource. Assign it to another user. Remember its id. + + WHEN user tomjon makes request POST /res + ... with header "Muck-User: verence" and body { "foo": "bar" } + THEN status code is 201 + THEN remember resource id as ID + THEN remember resource revision as REV1 + THEN response has header "Muck-Owner: verence" + +Retrieve the resource. + + WHEN user tomjon makes request GET /res with header "Muck-Id: ${ID}" + THEN status code is 200 + THEN response body is { "foo": "bar" } + THEN response has header "Muck-Id: ${ID}" + THEN response has header "Muck-Revision: ${REV1}" + THEN response has header "Muck-Owner: verence" + +Make sure Verence CAN retrieve, update, or delete the resource. + + WHEN user verence makes request GET /res with header "Muck-Id: ${ID}" + THEN status code is 200 + THEN response body is { "foo": "bar" } + THEN response has header "Muck-Id: ${ID}" + THEN response has header "Muck-Revision: ${REV1}" + THEN response has header "Muck-Owner: verence" + +All done. + + FINALLY Muck is stopped diff --git a/yarns/900-implements.yarn b/yarns/900-implements.yarn index c81d1ef..22d6463 100644 --- a/yarns/900-implements.yarn +++ b/yarns/900-implements.yarn @@ -12,6 +12,14 @@ IMPLEMENTS FINALLY Muck is stopped stop_muck() +## Create users + + + IMPLEMENTS GIVEN a user (\S+) with superuser access + user = get_next_match() + users = V['superusers'] or [] + V['superusers'] = users + [user] + ## HTTP requests IMPLEMENTS WHEN user (\S+) makes request POST /res with body (.*) @@ -19,6 +27,13 @@ body = get_expanded_match() POST(user, '/res', {}, json.loads(body)) + IMPLEMENTS WHEN user (\S+) makes request POST /res with header "(\S+): (.+)" and body (.*) + user = get_expanded_match() + header = get_expanded_match() + value = get_expanded_match() + body = get_expanded_match() + POST(user, '/res', {header:value}, json.loads(body)) + IMPLEMENTS WHEN user (\S+) makes request GET /res with header "(\S+): (.+)" user = get_expanded_match() header = get_expanded_match() diff --git a/yarns/lib.py b/yarns/lib.py index 583dff5..f64af8d 100644 --- a/yarns/lib.py +++ b/yarns/lib.py @@ -57,6 +57,7 @@ def start_muck(): pathname, config_filename, ] subprocess.check_call(argv) + time.sleep(2) V['base_url'] = 'http://127.0.0.1:{}'.format(12765) @@ -72,6 +73,8 @@ def create_test_token(sub): iss = 'test-issuer' aud = 'test-audience' scopes = ['create', 'update', 'show', 'delete'] + if sub in (V['superusers'] or []): + scopes.append('super') lifetime = 3600 return create_token(key_text, iss, aud, sub, scopes, lifetime) |