summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-11-16 21:36:04 +0200
committerLars Wirzenius <liw@liw.fi>2018-11-16 21:36:04 +0200
commit6d621d3a51ba68f0a436d5c5b27ace6cb2825f50 (patch)
tree6817d537d5475ccd3b8d888e20cdc51bd357f33e
parentd30c7d5dac5891ad86a3491e198cb384e466932e (diff)
downloadmuck-poc-6d621d3a51ba68f0a436d5c5b27ace6cb2825f50.tar.gz
Change: allow super users to impersonate other users
-rw-r--r--muck/request.py6
-rw-r--r--muck/request_tests.py11
-rwxr-xr-xmuck_poc18
-rw-r--r--yarns/200-super.yarn43
-rw-r--r--yarns/900-implements.yarn15
-rw-r--r--yarns/lib.py3
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')
diff --git a/muck_poc b/muck_poc
index 036389b..bb93832 100755
--- a/muck_poc
+++ b/muck_poc
@@ -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)