planar 0.9.3__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- planar/ai/agent.py +2 -1
- planar/ai/agent_base.py +24 -5
- planar/ai/state.py +17 -0
- planar/app.py +18 -1
- planar/data/connection.py +108 -0
- planar/data/dataset.py +11 -104
- planar/data/utils.py +89 -0
- planar/db/alembic/env.py +25 -1
- planar/files/storage/azure_blob.py +1 -1
- planar/registry_items.py +2 -0
- planar/routers/dataset_router.py +213 -0
- planar/routers/info.py +79 -36
- planar/routers/models.py +1 -0
- planar/routers/workflow.py +2 -0
- planar/scaffold_templates/pyproject.toml.j2 +1 -1
- planar/security/authorization.py +31 -3
- planar/security/default_policies.cedar +25 -0
- planar/testing/fixtures.py +34 -1
- planar/testing/planar_test_client.py +1 -1
- planar/workflows/decorators.py +2 -1
- planar/workflows/wrappers.py +1 -0
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/RECORD +25 -72
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/WHEEL +1 -1
- planar/ai/test_agent_serialization.py +0 -229
- planar/ai/test_agent_tool_step_display.py +0 -78
- planar/data/test_dataset.py +0 -354
- planar/files/storage/test_azure_blob.py +0 -435
- planar/files/storage/test_local_directory.py +0 -162
- planar/files/storage/test_s3.py +0 -299
- planar/files/test_files.py +0 -282
- planar/human/test_human.py +0 -385
- planar/logging/test_formatter.py +0 -327
- planar/modeling/mixins/test_auditable.py +0 -97
- planar/modeling/mixins/test_timestamp.py +0 -134
- planar/modeling/mixins/test_uuid_primary_key.py +0 -52
- planar/routers/test_agents_router.py +0 -174
- planar/routers/test_files_router.py +0 -49
- planar/routers/test_object_config_router.py +0 -367
- planar/routers/test_routes_security.py +0 -168
- planar/routers/test_rule_router.py +0 -470
- planar/routers/test_workflow_router.py +0 -539
- planar/rules/test_data/account_dormancy_management.json +0 -223
- planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
- planar/rules/test_data/applicant_risk_assessment.json +0 -435
- planar/rules/test_data/booking_fraud_detection.json +0 -407
- planar/rules/test_data/cellular_data_rollover_system.json +0 -258
- planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
- planar/rules/test_data/customer_lifetime_value.json +0 -143
- planar/rules/test_data/import_duties_calculator.json +0 -289
- planar/rules/test_data/insurance_prior_authorization.json +0 -443
- planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
- planar/rules/test_data/order_consolidation_system.json +0 -375
- planar/rules/test_data/portfolio_risk_monitor.json +0 -471
- planar/rules/test_data/supply_chain_risk.json +0 -253
- planar/rules/test_data/warehouse_cross_docking.json +0 -237
- planar/rules/test_rules.py +0 -1494
- planar/security/tests/test_auth_middleware.py +0 -162
- planar/security/tests/test_authorization_context.py +0 -78
- planar/security/tests/test_cedar_basics.py +0 -41
- planar/security/tests/test_cedar_policies.py +0 -158
- planar/security/tests/test_jwt_principal_context.py +0 -179
- planar/test_app.py +0 -142
- planar/test_cli.py +0 -394
- planar/test_config.py +0 -515
- planar/test_object_config.py +0 -527
- planar/test_object_registry.py +0 -14
- planar/test_sqlalchemy.py +0 -193
- planar/test_utils.py +0 -105
- planar/testing/test_memory_storage.py +0 -143
- planar/workflows/test_concurrency_detection.py +0 -120
- planar/workflows/test_lock_timeout.py +0 -140
- planar/workflows/test_serialization.py +0 -1203
- planar/workflows/test_suspend_deserialization.py +0 -231
- planar/workflows/test_workflow.py +0 -2005
- {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -1,162 +0,0 @@
|
|
1
|
-
from unittest.mock import Mock, patch
|
2
|
-
|
3
|
-
import pytest
|
4
|
-
from fastapi import FastAPI, HTTPException
|
5
|
-
from fastapi.responses import JSONResponse
|
6
|
-
|
7
|
-
from planar.security.auth_middleware import AuthMiddleware
|
8
|
-
|
9
|
-
|
10
|
-
@pytest.fixture
|
11
|
-
def app():
|
12
|
-
return FastAPI()
|
13
|
-
|
14
|
-
|
15
|
-
@pytest.fixture
|
16
|
-
def auth_middleware(app):
|
17
|
-
return AuthMiddleware(
|
18
|
-
app=app,
|
19
|
-
client_id="test-client-id",
|
20
|
-
org_id="test-org-id",
|
21
|
-
additional_exclusion_paths=["/test/exclude"],
|
22
|
-
service_token="plt_test-service-token",
|
23
|
-
)
|
24
|
-
|
25
|
-
|
26
|
-
class TestAuthMiddleware:
|
27
|
-
def test_org_id_validation_none(self, auth_middleware):
|
28
|
-
"""Test that org_id validation fails when token has None org_id"""
|
29
|
-
with (
|
30
|
-
patch.object(auth_middleware, "get_signing_key_from_jwt"),
|
31
|
-
patch("jwt.decode") as mock_decode,
|
32
|
-
):
|
33
|
-
mock_decode.return_value = {"org_id": None}
|
34
|
-
|
35
|
-
with pytest.raises(HTTPException) as exc_info:
|
36
|
-
auth_middleware.validate_jwt_token("fake-token")
|
37
|
-
|
38
|
-
assert exc_info.value.status_code == 401
|
39
|
-
assert exc_info.value.detail == "Invalid organization"
|
40
|
-
|
41
|
-
def test_org_id_validation_empty_string(self, auth_middleware):
|
42
|
-
"""Test that org_id validation fails when token has empty string org_id"""
|
43
|
-
with (
|
44
|
-
patch.object(auth_middleware, "get_signing_key_from_jwt"),
|
45
|
-
patch("jwt.decode") as mock_decode,
|
46
|
-
):
|
47
|
-
mock_decode.return_value = {"org_id": ""}
|
48
|
-
|
49
|
-
with pytest.raises(HTTPException) as exc_info:
|
50
|
-
auth_middleware.validate_jwt_token("fake-token")
|
51
|
-
|
52
|
-
assert exc_info.value.status_code == 401
|
53
|
-
assert exc_info.value.detail == "Invalid organization"
|
54
|
-
|
55
|
-
def test_org_id_validation_mismatch(self, auth_middleware):
|
56
|
-
"""Test that org_id validation fails when token org_id doesn't match"""
|
57
|
-
with (
|
58
|
-
patch.object(auth_middleware, "get_signing_key_from_jwt"),
|
59
|
-
patch("jwt.decode") as mock_decode,
|
60
|
-
):
|
61
|
-
mock_decode.return_value = {"org_id": "different-org-id"}
|
62
|
-
|
63
|
-
with pytest.raises(HTTPException) as exc_info:
|
64
|
-
auth_middleware.validate_jwt_token("fake-token")
|
65
|
-
|
66
|
-
assert exc_info.value.status_code == 401
|
67
|
-
assert exc_info.value.detail == "Invalid organization"
|
68
|
-
|
69
|
-
def test_org_id_validation_success(self, auth_middleware):
|
70
|
-
"""Test that org_id validation succeeds when token org_id matches"""
|
71
|
-
with (
|
72
|
-
patch.object(auth_middleware, "get_signing_key_from_jwt"),
|
73
|
-
patch("jwt.decode") as mock_decode,
|
74
|
-
):
|
75
|
-
expected_payload = {"org_id": "test-org-id", "user_id": "test-user"}
|
76
|
-
mock_decode.return_value = expected_payload
|
77
|
-
|
78
|
-
result = auth_middleware.validate_jwt_token("fake-token")
|
79
|
-
|
80
|
-
assert result == expected_payload
|
81
|
-
|
82
|
-
@pytest.mark.asyncio
|
83
|
-
async def test_service_token_validation_success(self, auth_middleware):
|
84
|
-
"""Test that service token validation succeeds when token matches"""
|
85
|
-
mock_request = Mock()
|
86
|
-
mock_request.url.path = "/planar/v1/something"
|
87
|
-
mock_request.headers = {"Authorization": "Bearer plt_test-service-token"}
|
88
|
-
mock_call_next = Mock()
|
89
|
-
mock_call_next.return_value = JSONResponse(
|
90
|
-
status_code=200, content={"message": "success"}
|
91
|
-
)
|
92
|
-
|
93
|
-
async def mock_call_next_func(request):
|
94
|
-
return mock_call_next(request)
|
95
|
-
|
96
|
-
result = await auth_middleware.dispatch(mock_request, mock_call_next_func)
|
97
|
-
mock_call_next.assert_called_once_with(mock_request)
|
98
|
-
assert result is not None
|
99
|
-
assert result.status_code == 200
|
100
|
-
|
101
|
-
@pytest.mark.asyncio
|
102
|
-
async def test_service_token_validation_failure(self, auth_middleware):
|
103
|
-
"""Test that service token validation succeeds when token matches"""
|
104
|
-
mock_request = Mock()
|
105
|
-
mock_request.url.path = "/planar/v1/something"
|
106
|
-
mock_request.headers = {"Authorization": "Bearer plt_wrong-token"}
|
107
|
-
mock_call_next = Mock()
|
108
|
-
mock_call_next.return_value = JSONResponse(
|
109
|
-
status_code=200, content={"message": "success"}
|
110
|
-
)
|
111
|
-
|
112
|
-
async def mock_call_next_func(request):
|
113
|
-
return mock_call_next(request)
|
114
|
-
|
115
|
-
result = await auth_middleware.dispatch(mock_request, mock_call_next_func)
|
116
|
-
mock_call_next.assert_not_called()
|
117
|
-
assert result is not None
|
118
|
-
assert result.status_code == 401
|
119
|
-
|
120
|
-
@pytest.mark.asyncio
|
121
|
-
async def test_exclusion_paths_includes_health_and_additional(self, app):
|
122
|
-
"""Test that exclusion paths include health endpoint and additional paths"""
|
123
|
-
middleware = AuthMiddleware(
|
124
|
-
app=app,
|
125
|
-
client_id="test-client-id",
|
126
|
-
org_id="test-org-id",
|
127
|
-
additional_exclusion_paths=["/custom/path", "/another/path"],
|
128
|
-
)
|
129
|
-
|
130
|
-
mock_request = Mock()
|
131
|
-
mock_call_next = Mock()
|
132
|
-
|
133
|
-
async def mock_call_next_func(request):
|
134
|
-
return mock_call_next(request)
|
135
|
-
|
136
|
-
expected_paths = ["/planar/v1/health", "/custom/path", "/another/path"]
|
137
|
-
for path in expected_paths:
|
138
|
-
mock_request.url.path = path
|
139
|
-
mock_call_next.reset_mock()
|
140
|
-
|
141
|
-
await middleware.dispatch(mock_request, mock_call_next_func) # type: ignore
|
142
|
-
|
143
|
-
mock_call_next.assert_called_once_with(mock_request)
|
144
|
-
|
145
|
-
# Test that non-excluded paths are not excluded
|
146
|
-
mock_request.url.path = "/not-excluded"
|
147
|
-
mock_call_next.reset_mock()
|
148
|
-
result = await middleware.dispatch(mock_request, mock_call_next_func)
|
149
|
-
assert result is not None
|
150
|
-
assert result.status_code == 401
|
151
|
-
mock_call_next.assert_not_called()
|
152
|
-
|
153
|
-
def test_required_org_id_parameter(self, app):
|
154
|
-
"""Test that org_id parameter is required (not optional)"""
|
155
|
-
# This test ensures org_id cannot be None based on type hints
|
156
|
-
# The actual enforcement is at the type level, so we just verify
|
157
|
-
# the constructor works with a valid org_id
|
158
|
-
middleware = AuthMiddleware(
|
159
|
-
app=app, client_id="test-client-id", org_id="required-org-id"
|
160
|
-
)
|
161
|
-
|
162
|
-
assert middleware.org_id == "required-org-id"
|
@@ -1,78 +0,0 @@
|
|
1
|
-
from planar.security.auth_context import Principal
|
2
|
-
from planar.security.authorization import (
|
3
|
-
CedarEntity,
|
4
|
-
PolicyService,
|
5
|
-
ResourceType,
|
6
|
-
WorkflowAction,
|
7
|
-
get_policy_service,
|
8
|
-
policy_service_context,
|
9
|
-
set_policy_service,
|
10
|
-
)
|
11
|
-
|
12
|
-
|
13
|
-
def test_policy_service_context_variable():
|
14
|
-
"""Test that the authorization service context variable works correctly."""
|
15
|
-
# Initially, no authz service should be set
|
16
|
-
assert get_policy_service() is None
|
17
|
-
|
18
|
-
# Create a mock policy service
|
19
|
-
policy_service = PolicyService()
|
20
|
-
|
21
|
-
# Set the policy service in context
|
22
|
-
set_policy_service(policy_service)
|
23
|
-
assert get_policy_service() is policy_service
|
24
|
-
|
25
|
-
# Reset the context
|
26
|
-
set_policy_service(None)
|
27
|
-
assert get_policy_service() is None
|
28
|
-
|
29
|
-
# Test the context manager
|
30
|
-
async def test_context_manager():
|
31
|
-
async with policy_service_context(policy_service):
|
32
|
-
assert get_policy_service() is policy_service
|
33
|
-
# After exiting the context, it should be None again
|
34
|
-
assert get_policy_service() is None
|
35
|
-
|
36
|
-
# Run the async test
|
37
|
-
import asyncio
|
38
|
-
|
39
|
-
asyncio.run(test_context_manager())
|
40
|
-
|
41
|
-
|
42
|
-
def test_policy_service_with_principal():
|
43
|
-
"""Test that the policy service works with principal resources."""
|
44
|
-
policy_service = PolicyService()
|
45
|
-
|
46
|
-
# Create a mock principal with only required fields
|
47
|
-
principal = Principal(sub="test-user") # type: ignore
|
48
|
-
|
49
|
-
# Create resources
|
50
|
-
principal_resource = CedarEntity.from_principal(principal)
|
51
|
-
workflow_resource = CedarEntity.from_workflow("test_workflow")
|
52
|
-
|
53
|
-
# Test that the service can be used
|
54
|
-
assert principal_resource.resource_type == ResourceType.PRINCIPAL
|
55
|
-
assert workflow_resource.resource_type == ResourceType.WORKFLOW
|
56
|
-
assert principal_resource.resource_attributes["sub"] == "test-user"
|
57
|
-
assert workflow_resource.resource_attributes["function_name"] == "test_workflow"
|
58
|
-
|
59
|
-
assert (
|
60
|
-
policy_service.is_allowed(
|
61
|
-
principal_resource, WorkflowAction.WORKFLOW_RUN, workflow_resource
|
62
|
-
)
|
63
|
-
is True
|
64
|
-
)
|
65
|
-
|
66
|
-
assert (
|
67
|
-
policy_service.is_allowed(
|
68
|
-
principal_resource, WorkflowAction.WORKFLOW_RUN, workflow_resource
|
69
|
-
)
|
70
|
-
is True
|
71
|
-
)
|
72
|
-
|
73
|
-
assert (
|
74
|
-
policy_service.is_allowed(
|
75
|
-
principal_resource, "Workflow::Fail", workflow_resource
|
76
|
-
)
|
77
|
-
is False
|
78
|
-
)
|
@@ -1,41 +0,0 @@
|
|
1
|
-
from cedarpy import Decision, format_policies, is_authorized
|
2
|
-
|
3
|
-
|
4
|
-
def test_cedar_permissions():
|
5
|
-
# Define entities
|
6
|
-
entities = [
|
7
|
-
{
|
8
|
-
"uid": {"__entity": {"type": "Principal", "id": "alice"}},
|
9
|
-
"attrs": {},
|
10
|
-
"parents": [],
|
11
|
-
},
|
12
|
-
{
|
13
|
-
"uid": {"__entity": {"type": "Document", "id": "doc1"}},
|
14
|
-
"attrs": {},
|
15
|
-
"parents": [],
|
16
|
-
},
|
17
|
-
]
|
18
|
-
|
19
|
-
# Initialize the Cedar policy service
|
20
|
-
policy = """
|
21
|
-
permit (
|
22
|
-
principal,
|
23
|
-
action == Action::"Run",
|
24
|
-
resource
|
25
|
-
);
|
26
|
-
"""
|
27
|
-
formatted_policy = format_policies(policy)
|
28
|
-
print(formatted_policy)
|
29
|
-
|
30
|
-
# Create a simple request
|
31
|
-
request = {
|
32
|
-
"principal": 'Principal::"alice"',
|
33
|
-
"action": 'Action::"Run"',
|
34
|
-
"resource": 'Document::"doc1"',
|
35
|
-
}
|
36
|
-
|
37
|
-
# Test the authorization
|
38
|
-
result = is_authorized(request, policy, entities)
|
39
|
-
assert result.decision == Decision.Allow, (
|
40
|
-
"Cedar works and basic permission should be allowed"
|
41
|
-
)
|
@@ -1,158 +0,0 @@
|
|
1
|
-
import pytest
|
2
|
-
|
3
|
-
from planar.security.auth_context import Principal
|
4
|
-
from planar.security.authorization import (
|
5
|
-
AgentAction,
|
6
|
-
CedarEntity,
|
7
|
-
PolicyService,
|
8
|
-
RuleAction,
|
9
|
-
WorkflowAction,
|
10
|
-
)
|
11
|
-
|
12
|
-
|
13
|
-
@pytest.fixture
|
14
|
-
def policy_service():
|
15
|
-
return PolicyService()
|
16
|
-
|
17
|
-
|
18
|
-
def test_workflow_permissions(policy_service: PolicyService):
|
19
|
-
# Create a test principal (user)
|
20
|
-
user_principal = Principal(
|
21
|
-
sub="user123" # type: ignore
|
22
|
-
)
|
23
|
-
|
24
|
-
# Create test resources
|
25
|
-
workflow_resource = CedarEntity.from_workflow("com.example.workflow.ProcessData")
|
26
|
-
|
27
|
-
# Test Workflow Actions
|
28
|
-
assert policy_service.is_allowed(
|
29
|
-
CedarEntity.from_principal(user_principal),
|
30
|
-
WorkflowAction.WORKFLOW_LIST,
|
31
|
-
workflow_resource,
|
32
|
-
), "User should be able to list workflows"
|
33
|
-
|
34
|
-
assert policy_service.is_allowed(
|
35
|
-
CedarEntity.from_principal(user_principal),
|
36
|
-
WorkflowAction.WORKFLOW_VIEW_DETAILS,
|
37
|
-
workflow_resource,
|
38
|
-
), "User should be able to view workflow details"
|
39
|
-
|
40
|
-
assert policy_service.is_allowed(
|
41
|
-
CedarEntity.from_principal(user_principal),
|
42
|
-
WorkflowAction.WORKFLOW_RUN,
|
43
|
-
workflow_resource,
|
44
|
-
), "User should be able to run workflow"
|
45
|
-
|
46
|
-
assert policy_service.is_allowed(
|
47
|
-
CedarEntity.from_principal(user_principal),
|
48
|
-
WorkflowAction.WORKFLOW_CANCEL,
|
49
|
-
workflow_resource,
|
50
|
-
), "User should be able to cancel workflow"
|
51
|
-
|
52
|
-
|
53
|
-
def test_agent_permissions(policy_service: PolicyService):
|
54
|
-
user_principal = Principal(
|
55
|
-
sub="user123" # type: ignore
|
56
|
-
)
|
57
|
-
|
58
|
-
agent_resource = CedarEntity.from_agent("OcrAgent")
|
59
|
-
|
60
|
-
# Test Agent Actions
|
61
|
-
assert policy_service.is_allowed(
|
62
|
-
CedarEntity.from_principal(user_principal),
|
63
|
-
AgentAction.AGENT_LIST,
|
64
|
-
agent_resource,
|
65
|
-
), "User should be able to list agents"
|
66
|
-
|
67
|
-
assert policy_service.is_allowed(
|
68
|
-
CedarEntity.from_principal(user_principal),
|
69
|
-
AgentAction.AGENT_VIEW_DETAILS,
|
70
|
-
agent_resource,
|
71
|
-
), "User should be able to view agent details"
|
72
|
-
|
73
|
-
assert policy_service.is_allowed(
|
74
|
-
CedarEntity.from_principal(user_principal),
|
75
|
-
AgentAction.AGENT_RUN,
|
76
|
-
agent_resource,
|
77
|
-
), "User should be able to run agent"
|
78
|
-
|
79
|
-
assert policy_service.is_allowed(
|
80
|
-
CedarEntity.from_principal(user_principal),
|
81
|
-
AgentAction.AGENT_UPDATE,
|
82
|
-
agent_resource,
|
83
|
-
), "User should be able to configure agent"
|
84
|
-
|
85
|
-
|
86
|
-
def test_denied_permission(policy_service: PolicyService):
|
87
|
-
# Create a test principal (user)
|
88
|
-
user_principal = Principal(
|
89
|
-
sub="user123" # type: ignore
|
90
|
-
)
|
91
|
-
|
92
|
-
# Create test resource
|
93
|
-
workflow_resource = CedarEntity.from_workflow("com.example.workflow.ProcessData")
|
94
|
-
|
95
|
-
# Test that a non-existent action is denied
|
96
|
-
assert not policy_service.is_allowed(
|
97
|
-
CedarEntity.from_principal(user_principal),
|
98
|
-
"Workflow::Delete", # This action is not defined in our policies
|
99
|
-
workflow_resource,
|
100
|
-
), "User should not be able to delete workflows"
|
101
|
-
|
102
|
-
|
103
|
-
def test_deny_edits_to_rules_for_member_role(policy_service: PolicyService):
|
104
|
-
member_jwt_data = {
|
105
|
-
"org_name": "CoPlane",
|
106
|
-
"user_first_name": "Donald",
|
107
|
-
"user_last_name": "Knuth",
|
108
|
-
"user_email": "don@coplane.com",
|
109
|
-
"iss": "https://auth-api.coplane.com",
|
110
|
-
"sub": "user_02JYMGMYETXMAVB0GKT868T8V7",
|
111
|
-
"sid": "session_01JZ6NJVC1MSR86VZNR54BF9D4",
|
112
|
-
"jti": "01JZ8EHD793F6RTY8FC4A3H4E9",
|
113
|
-
"org_id": "org_01JY4QP57Y7H4EQ7HT3BGN7TNK",
|
114
|
-
"role": "member",
|
115
|
-
"permissions": [],
|
116
|
-
"feature_flags": [],
|
117
|
-
"exp": 1751556901,
|
118
|
-
"iat": 1751556601,
|
119
|
-
}
|
120
|
-
|
121
|
-
user_principal = Principal.from_jwt_payload(member_jwt_data)
|
122
|
-
|
123
|
-
rule_resource = CedarEntity.from_rule("complex_business_rule")
|
124
|
-
|
125
|
-
assert not policy_service.is_allowed(
|
126
|
-
CedarEntity.from_principal(user_principal),
|
127
|
-
RuleAction.RULE_UPDATE,
|
128
|
-
rule_resource,
|
129
|
-
)
|
130
|
-
|
131
|
-
|
132
|
-
def test_allow_edits_to_rules_for_admin_role(policy_service: PolicyService):
|
133
|
-
member_jwt_data = {
|
134
|
-
"org_name": "CoPlane",
|
135
|
-
"user_first_name": "Donald",
|
136
|
-
"user_last_name": "Knuth",
|
137
|
-
"user_email": "don@coplane.com",
|
138
|
-
"iss": "https://auth-api.coplane.com",
|
139
|
-
"sub": "user_02JYMGMYETXMAVB0GKT868T8V7",
|
140
|
-
"sid": "session_01JZ6NJVC1MSR86VZNR54BF9D4",
|
141
|
-
"jti": "01JZ8EHD793F6RTY8FC4A3H4E9",
|
142
|
-
"org_id": "org_01JY4QP57Y7H4EQ7HT3BGN7TNK",
|
143
|
-
"role": "admin",
|
144
|
-
"permissions": [],
|
145
|
-
"feature_flags": [],
|
146
|
-
"exp": 1751556901,
|
147
|
-
"iat": 1751556601,
|
148
|
-
}
|
149
|
-
|
150
|
-
user_principal = Principal.from_jwt_payload(member_jwt_data)
|
151
|
-
|
152
|
-
rule_resource = CedarEntity.from_rule("complex_business_rule")
|
153
|
-
|
154
|
-
assert policy_service.is_allowed(
|
155
|
-
CedarEntity.from_principal(user_principal),
|
156
|
-
RuleAction.RULE_UPDATE,
|
157
|
-
rule_resource,
|
158
|
-
)
|
@@ -1,179 +0,0 @@
|
|
1
|
-
import pytest
|
2
|
-
|
3
|
-
from planar.security.auth_context import (
|
4
|
-
Principal,
|
5
|
-
clear_principal,
|
6
|
-
get_current_principal,
|
7
|
-
has_role,
|
8
|
-
require_principal,
|
9
|
-
set_principal,
|
10
|
-
)
|
11
|
-
|
12
|
-
|
13
|
-
def test_principal_from_jwt_payload():
|
14
|
-
# Test with minimal required fields
|
15
|
-
payload = {"sub": "user123"}
|
16
|
-
principal = Principal.from_jwt_payload(payload)
|
17
|
-
assert principal.sub == "user123"
|
18
|
-
assert principal.extra_claims == {}
|
19
|
-
|
20
|
-
# Test with all standard fields
|
21
|
-
payload = {
|
22
|
-
"sub": "user123",
|
23
|
-
"iss": "https://auth-api.coplane.com",
|
24
|
-
"exp": 1234567890,
|
25
|
-
"iat": 1234567890,
|
26
|
-
"sid": "session123",
|
27
|
-
"jti": "jwt123",
|
28
|
-
"org_id": "org123",
|
29
|
-
"org_name": "Test Org",
|
30
|
-
"user_first_name": "John",
|
31
|
-
"user_last_name": "Doe",
|
32
|
-
"user_email": "john@example.com",
|
33
|
-
"role": "admin",
|
34
|
-
"permissions": ["read", "write"],
|
35
|
-
}
|
36
|
-
principal = Principal.from_jwt_payload(payload)
|
37
|
-
assert principal.sub == "user123"
|
38
|
-
assert principal.iss == "https://auth-api.coplane.com"
|
39
|
-
assert principal.exp == 1234567890
|
40
|
-
assert principal.iat == 1234567890
|
41
|
-
assert principal.sid == "session123"
|
42
|
-
assert principal.jti == "jwt123"
|
43
|
-
assert principal.org_id == "org123"
|
44
|
-
assert principal.org_name == "Test Org"
|
45
|
-
assert principal.user_first_name == "John"
|
46
|
-
assert principal.user_last_name == "Doe"
|
47
|
-
assert principal.user_email == "john@example.com"
|
48
|
-
assert principal.role == "admin"
|
49
|
-
assert principal.permissions == ["read", "write"]
|
50
|
-
assert principal.extra_claims == {}
|
51
|
-
|
52
|
-
# Test with extra claims
|
53
|
-
payload = {
|
54
|
-
"sub": "user123",
|
55
|
-
"custom_field": "custom_value",
|
56
|
-
"another_field": 123,
|
57
|
-
}
|
58
|
-
principal = Principal.from_jwt_payload(payload)
|
59
|
-
assert principal.sub == "user123"
|
60
|
-
assert principal.extra_claims == {
|
61
|
-
"custom_field": "custom_value",
|
62
|
-
"another_field": 123,
|
63
|
-
}
|
64
|
-
|
65
|
-
# Test with missing required field
|
66
|
-
with pytest.raises(ValueError, match="JWT payload must contain 'sub' field"):
|
67
|
-
Principal.from_jwt_payload({})
|
68
|
-
|
69
|
-
|
70
|
-
def test_get_current_principal():
|
71
|
-
# Test when no principal is set
|
72
|
-
assert get_current_principal() is None
|
73
|
-
|
74
|
-
# Test when principal is set
|
75
|
-
principal = Principal(
|
76
|
-
sub="user123",
|
77
|
-
iss=None,
|
78
|
-
exp=None,
|
79
|
-
iat=None,
|
80
|
-
sid=None,
|
81
|
-
jti=None,
|
82
|
-
org_id=None,
|
83
|
-
org_name=None,
|
84
|
-
user_first_name=None,
|
85
|
-
user_last_name=None,
|
86
|
-
user_email=None,
|
87
|
-
role=None,
|
88
|
-
permissions=None,
|
89
|
-
)
|
90
|
-
token = set_principal(principal)
|
91
|
-
try:
|
92
|
-
assert get_current_principal() == principal
|
93
|
-
finally:
|
94
|
-
clear_principal(token)
|
95
|
-
|
96
|
-
# Verify principal is cleared
|
97
|
-
assert get_current_principal() is None
|
98
|
-
|
99
|
-
|
100
|
-
def test_has_role():
|
101
|
-
# Test when no principal is set
|
102
|
-
assert not has_role("admin")
|
103
|
-
|
104
|
-
# Test when principal has matching role
|
105
|
-
principal = Principal(
|
106
|
-
sub="user123",
|
107
|
-
role="admin",
|
108
|
-
iss=None,
|
109
|
-
exp=None,
|
110
|
-
iat=None,
|
111
|
-
sid=None,
|
112
|
-
jti=None,
|
113
|
-
org_id=None,
|
114
|
-
org_name=None,
|
115
|
-
user_first_name=None,
|
116
|
-
user_last_name=None,
|
117
|
-
user_email=None,
|
118
|
-
permissions=None,
|
119
|
-
)
|
120
|
-
token = set_principal(principal)
|
121
|
-
try:
|
122
|
-
assert has_role("admin")
|
123
|
-
assert not has_role("user")
|
124
|
-
finally:
|
125
|
-
clear_principal(token)
|
126
|
-
|
127
|
-
# Test when principal has no role
|
128
|
-
principal = Principal(
|
129
|
-
sub="user123",
|
130
|
-
iss=None,
|
131
|
-
exp=None,
|
132
|
-
iat=None,
|
133
|
-
sid=None,
|
134
|
-
jti=None,
|
135
|
-
org_id=None,
|
136
|
-
org_name=None,
|
137
|
-
user_first_name=None,
|
138
|
-
user_last_name=None,
|
139
|
-
user_email=None,
|
140
|
-
role=None,
|
141
|
-
permissions=None,
|
142
|
-
)
|
143
|
-
token = set_principal(principal)
|
144
|
-
try:
|
145
|
-
assert not has_role("admin")
|
146
|
-
finally:
|
147
|
-
clear_principal(token)
|
148
|
-
|
149
|
-
|
150
|
-
def test_require_principal():
|
151
|
-
# Test when no principal is set
|
152
|
-
with pytest.raises(RuntimeError, match="No authenticated principal in context"):
|
153
|
-
require_principal()
|
154
|
-
|
155
|
-
# Test when principal is set
|
156
|
-
principal = Principal(
|
157
|
-
sub="user123",
|
158
|
-
iss=None,
|
159
|
-
exp=None,
|
160
|
-
iat=None,
|
161
|
-
sid=None,
|
162
|
-
jti=None,
|
163
|
-
org_id=None,
|
164
|
-
org_name=None,
|
165
|
-
user_first_name=None,
|
166
|
-
user_last_name=None,
|
167
|
-
user_email=None,
|
168
|
-
role=None,
|
169
|
-
permissions=None,
|
170
|
-
)
|
171
|
-
token = set_principal(principal)
|
172
|
-
try:
|
173
|
-
assert require_principal() == principal
|
174
|
-
finally:
|
175
|
-
clear_principal(token)
|
176
|
-
|
177
|
-
# Verify principal is cleared
|
178
|
-
with pytest.raises(RuntimeError, match="No authenticated principal in context"):
|
179
|
-
require_principal()
|