import time from dataclasses import replace from urllib.parse import parse_qs, urlsplit from test_journey_roles import JourneyFixture from test_web import invoke class PolicyFixture: def __init__(self): self.calls=[]; self.unavailable=False def call(self, token, body): self.calls.append((token,body)) if self.unavailable: raise RuntimeError('private-provider-error') result=dict(success=True,status='current',revision=1,clients=[dict(id='vergabe-demo-company',name='Demo Company',mode='optional_after_enrollment')],history=[]) if body['action'] in {'preview','rollback'}: result.update(status='preview',client='vergabe-demo-company',before='optional_after_enrollment',after='mandatory',reference='p06-case',confirmation='fixture-confirmation') if body['action']=='apply': result.update(status='recorded',receipt=dict(client='vergabe-demo-company',after='mandatory',reference='p06-case')) return result class AuthenticationPolicyJourney(JourneyFixture): def setUp(self): super().setUp(); self.provider=PolicyFixture(); self.app.authentication_policy=self.provider session=self.oidc.sessions['operator'] self.oidc.sessions['operator']=replace(session,id_token='server-only-id-token',claims=dict(session.claims,assurance=dict(level='aal2',mfa=True,at=time.time()))) def test_aal1_account_access_does_not_grant_administration(self): for who in ['member','admin','operator']: self.oidc.sessions[who].claims['assurance']=dict(level='aal1',mfa=False) for path in ['/','/onboarding','/security']: response,_=invoke(self.app,path,cookie='ue_session='+who) self.assertEqual('200 OK',response['status']) for path in ['/platform','/admin/tenant:trial:demo-company','/platform/authentication-policy']: _,body=invoke(self.app,path,cookie='ue_session='+who) self.assertIn(b'Verify with MFA to administer',body) response,_=self.post('/api/v1/platform/tenants',who=who) self.assertEqual('403 Forbidden',response['status']) self.assertEqual([],self.provider.calls) self.assertEqual([],self.app.provisioning.actions) def test_role_csrf_and_stale_mfa_deny_before_policy_service(self): for who in ['member','admin']: response,_=self.post('/platform/authentication-policy',who=who,action='preview') self.assertEqual('403 Forbidden',response['status']) response,_=self.post('/platform/authentication-policy',who='operator',csrf_token='wrong',action='preview') self.assertEqual('403 Forbidden',response['status']) self.oidc.sessions['operator'].claims['assurance']['at']=time.time()-301 _,body=invoke(self.app,'/platform/authentication-policy',cookie='ue_session=operator') self.assertIn(b'fresh MFA sign-in',body);self.assertEqual([],self.provider.calls) def test_review_explains_lockout_scope_rollback_and_receipts(self): _,body=self.post('/platform/authentication-policy',who='operator',action='preview',client='vergabe-demo-company',mode='mandatory',reference='p06-case') self.assertIn(b'Review policy change',body);self.assertIn(b'unable to complete sign-in',body) self.assertIn(b'does not revoke already issued tokens',body);self.assertIn(b'Cancel without changes',body) self.assertNotIn(b'server-only-id-token',body) self.assertEqual('preview',self.provider.calls[-1][1]['action']) _,body=self.post('/platform/authentication-policy',who='operator',action='apply',confirmation='fixture-confirmation',acknowledged='yes') self.assertIn(b'Policy change recorded',body);self.assertIn(b'p06-case',body) self.assertTrue(self.provider.calls[-1][1]['acknowledged']) _,body=self.post('/platform/authentication-policy',who='operator',action='rollback',client='vergabe-demo-company',reference='rollback') self.assertIn(b'Review policy change',body) def test_outage_is_redacted_and_current_status_is_retryable(self): self.provider.unavailable=True _,body=invoke(self.app,'/platform/authentication-policy',cookie='ue_session=operator') self.assertIn(b'Existing policy remains in force',body);self.assertNotIn(b'private-provider-error',body) self.provider.unavailable=False _,body=invoke(self.app,'/platform/authentication-policy',cookie='ue_session=operator') self.assertIn(b'Current policy',body) def test_policy_stepup_binds_only_supported_return_path(self): response,_=invoke(self.app,'/login',query='recovery=1&return_path=/platform/authentication-policy') query=parse_qs(urlsplit(dict(response['headers'])['Location']).query) self.assertEqual(['aal2'],query['acr_values']);self.assertEqual(['login'],query['prompt']) self.assertEqual('/platform/authentication-policy',self.oidc.pending[query['state'][0]].return_path) response,_=invoke(self.app,'/login',query='recovery=1&return_path=https://evil.example') self.assertEqual('400 Bad Request',response['status'])