105 lines
4.2 KiB
Python
105 lines
4.2 KiB
Python
"""Sessions: scoping, expiry, and the invariant that matters."""
|
|
|
|
import base64
|
|
import unittest
|
|
|
|
from sigil import errors, identity, keys, sessions, viewcodes
|
|
from sigil.store import Store
|
|
|
|
import helpers
|
|
|
|
|
|
class Base(unittest.TestCase):
|
|
def setUp(self):
|
|
self.store = Store(":memory:")
|
|
self.private, self.public = keys.generate()
|
|
secret = identity.seed_bootstrap(self.store, "bigpickle")
|
|
identity.enrol(
|
|
self.store, "bigpickle", keys.export_public_b64(self.public), secret
|
|
)
|
|
|
|
def issue(self, agent="bigpickle", **kw):
|
|
return helpers.issue(self.store, self.private, agent, **kw)
|
|
|
|
def agent_session(self, **kw):
|
|
chal = identity.challenge(self.store, "bigpickle")
|
|
sig = base64.b64encode(self.private.sign(chal["nonce"].encode())).decode()
|
|
return identity.verify(self.store, chal["nonce"], sig, **kw)
|
|
|
|
|
|
class TestAgentSessions(Base):
|
|
def test_agent_may_act_as_agent(self):
|
|
session = sessions.resolve(self.store, self.agent_session()["token"])
|
|
self.assertEqual(sessions.assert_may_act_as_agent(session), "bigpickle")
|
|
|
|
def test_expired_session_refused(self):
|
|
token = self.agent_session(ttl=-1)["token"]
|
|
with self.assertRaises(errors.SessionExpired):
|
|
sessions.resolve(self.store, token)
|
|
|
|
def test_revoked_session_refused(self):
|
|
token = self.agent_session()["token"]
|
|
sessions.revoke(self.store, token)
|
|
with self.assertRaises(errors.SessionExpired):
|
|
sessions.resolve(self.store, token)
|
|
|
|
def test_unknown_token_refused(self):
|
|
with self.assertRaises(errors.SessionUnknown):
|
|
sessions.resolve(self.store, "nope")
|
|
|
|
def test_scope_denied(self):
|
|
session = sessions.resolve(self.store, self.agent_session()["token"])
|
|
with self.assertRaises(errors.ScopeDenied):
|
|
sessions.require_scope(session, "secret:admin")
|
|
|
|
def test_cannot_mint_session_with_invented_scope(self):
|
|
with self.assertRaises(errors.ScopeDenied):
|
|
sessions.issue_agent_session(self.store, "bigpickle", scopes=["root:all"])
|
|
|
|
|
|
class TestHumanSessions(Base):
|
|
def test_human_session_cannot_act_as_agent(self):
|
|
code = self.issue("bigpickle")
|
|
view = viewcodes.redeem_code(self.store, code["code"])
|
|
session = sessions.resolve(self.store, view["token"])
|
|
with self.assertRaises(errors.AgentImpersonationDenied):
|
|
sessions.assert_may_act_as_agent(session)
|
|
|
|
def test_human_session_is_typed_and_attributed(self):
|
|
code = self.issue("bigpickle")
|
|
view = viewcodes.redeem_code(self.store, code["code"])
|
|
session = sessions.resolve(self.store, view["token"])
|
|
self.assertEqual(session["kind"], "human_via_agent")
|
|
self.assertEqual(session["on_behalf_of"], "bigpickle")
|
|
self.assertEqual(session["granted_by"], "bigpickle")
|
|
self.assertEqual(session["subject"], "bigpickle's human")
|
|
|
|
def test_human_session_can_view(self):
|
|
code = self.issue("bigpickle")
|
|
view = viewcodes.redeem_code(self.store, code["code"])
|
|
session = sessions.resolve(self.store, view["token"])
|
|
sessions.require_scope(session, "view:read")
|
|
|
|
def test_view_session_cannot_be_minted_with_write_scope(self):
|
|
with self.assertRaises(errors.ScopeDenied):
|
|
self.issue("bigpickle", scopes=["view:read", "view:write"])
|
|
|
|
def test_view_session_cannot_be_minted_with_agent_act_scope(self):
|
|
with self.assertRaises(errors.ScopeDenied):
|
|
self.issue("bigpickle", scopes=["agent:act"])
|
|
|
|
def test_long_session_grants_no_extra_capability(self):
|
|
"""Expiry and authority are separate axes."""
|
|
short = self.issue("bigpickle")
|
|
short_session = viewcodes.redeem_code(self.store, short["code"])
|
|
# 24 hours of access, redeemed the same way a 5-minute code would be.
|
|
long_issued = self.issue("bigpickle", code_ttl=300, view_ttl=86400)
|
|
long_session = viewcodes.redeem_code(self.store, long_issued["code"])
|
|
self.assertEqual(
|
|
sorted(sessions.resolve(self.store, short_session["token"])["scopes"]),
|
|
sorted(sessions.resolve(self.store, long_session["token"])["scopes"]),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |