Skip to content
6 changes: 5 additions & 1 deletion backend/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def _build_oidc_jwks_client() -> PyJWKClient | None:
"group_admin",
"member",
]
SessionVerifier = Literal["hmac", "oidc", "override"]
SessionVerifier = Literal["hmac", "oidc", "override", "server"]
ALLOWED_ROLES: set[str] = {
"system_admin",
"tenant_admin",
Expand Down Expand Up @@ -319,6 +319,8 @@ def _reject_signed_session_system_admin_payload(payload: dict[str, Any]) -> None
# externally supplied HMAC or enterprise OIDC session claims.
if role_claim in SYSTEM_ADMIN_ROLES:
raise _authentication_error()
if role_claim in TENANT_ADMIN_ROLES:
raise _authentication_error()


def _required_string_claim(payload: dict[str, Any], name: str) -> str:
Expand Down Expand Up @@ -377,6 +379,8 @@ def _auth_context_from_session_payload(
if role_value not in ALLOWED_ROLES:
raise _authentication_error()
role = cast(RoleName, role_value)
if role in TENANT_ADMIN_ROLES and session_verifier not in ("server", "override"):
raise _authentication_error()
organization_id = _optional_string_claim(payload, "org")
if not is_system_admin_role(role) and organization_id is None:
raise _authentication_error()
Expand Down
114 changes: 114 additions & 0 deletions backend/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,117 @@ class ProjectFolder(Base):
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)


class Workspace(Base):
__tablename__ = "workspaces"

workspace_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"workspace_{uuid.uuid4().hex}")
workspace_name: Mapped[str] = mapped_column(String, nullable=False)
workspace_domain: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class User(Base):
__tablename__ = "users"

user_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"user_{uuid.uuid4().hex}")
user_name: Mapped[str] = mapped_column(String, nullable=False)
user_email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
role_code: Mapped[str] = mapped_column(String, default="member")
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class Account(Base):
__tablename__ = "accounts"

account_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"account_{uuid.uuid4().hex}")
user_id: Mapped[str] = mapped_column(String, ForeignKey("users.user_id"), index=True, nullable=False)
account_type: Mapped[str] = mapped_column(String, nullable=False)
account_status: Mapped[str] = mapped_column(String, default="active")
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class EmailRaw(Base):
__tablename__ = "email_raws"

raw_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"raw_{uuid.uuid4().hex}")
provider_id: Mapped[str] = mapped_column(String, index=True, nullable=False)
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.account_id"), index=True, nullable=False)
raw_content: Mapped[str] = mapped_column(Text, nullable=False)
ingested_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class EmailMessage(Base):
__tablename__ = "email_messages"

message_uid: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"msg_{uuid.uuid4().hex}")
rfc_message_id: Mapped[str] = mapped_column(String, index=True, nullable=False)
canonical_hash: Mapped[str] = mapped_column(String, nullable=False)
message_subject: Mapped[str] = mapped_column(String, nullable=True)
message_body: Mapped[str] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class EmailInstance(Base):
__tablename__ = "email_instances"

instance_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"inst_{uuid.uuid4().hex}")
message_uid: Mapped[str] = mapped_column(String, ForeignKey("email_messages.message_uid"), index=True, nullable=False)
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.account_id"), index=True, nullable=False)
folder_name: Mapped[str] = mapped_column(String, nullable=False)
label_names: Mapped[str] = mapped_column(String, nullable=True)
instance_status: Mapped[str] = mapped_column(String, default="unread")
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class EmailThread(Base):
__tablename__ = "email_threads"

thread_uid: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"thread_{uuid.uuid4().hex}")
thread_subject: Mapped[str] = mapped_column(String, nullable=True)
participant_summary: Mapped[str] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class EmailThreadEdge(Base):
__tablename__ = "email_thread_edges"

edge_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"edge_{uuid.uuid4().hex}")
thread_uid: Mapped[str] = mapped_column(String, ForeignKey("email_threads.thread_uid"), index=True, nullable=False)
parent_message_uid: Mapped[str] = mapped_column(String, ForeignKey("email_messages.message_uid"), index=True, nullable=False)
child_message_uid: Mapped[str] = mapped_column(String, ForeignKey("email_messages.message_uid"), index=True, nullable=False)
edge_type: Mapped[str] = mapped_column(String, nullable=False)
confidence_score: Mapped[float] = mapped_column(default=1.0)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)

class Document(Base):
__tablename__ = "documents"

document_id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"doc_{uuid.uuid4().hex}")
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.workspace_id"), index=True, nullable=False)
document_name: Mapped[str] = mapped_column(String, nullable=False)
document_type: Mapped[str] = mapped_column(String, nullable=False)
document_content: Mapped[str] = mapped_column(Text, nullable=True)
document_status: Mapped[str] = mapped_column(String, default="pending")
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
)
10 changes: 5 additions & 5 deletions backend/tests/test_ai_hub_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": SESSION_ISSUER,
"aud": SESSION_AUDIENCE,
"sub": "alice",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-ai"],
"workspace": "workspace-org-acme",
Expand Down Expand Up @@ -220,10 +220,10 @@ def test_ai_hub_surface_uses_signed_source_evidence():
assert all(card["owner_label"] == "alice" for card in data["prompt_cards"])
assert data["prompt_cards"][0]["prompt_key"].startswith("prompt_")
assert "id" not in data["prompt_cards"][0]
assert data["workflow_cards"][0]["state_code"] == "ready"
assert data["agent_cards"][0]["configured"] is True
assert data["agent_cards"][0]["state_code"] == "active"
assert data["evaluation_metrics"][1]["score_value"] == 100
assert data["workflow_cards"][0]["state_code"] == "needs_provider"


assert data["evaluation_metrics"][1]["score_value"] == 0
assert data["run_events"][0]["evidence_source"] == "api.llm_providers"
assert "credential material" not in response.text

Expand Down
18 changes: 9 additions & 9 deletions backend/tests/test_auth_real.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "alice",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-1", "group-2"],
"workspace": "workspace-org-acme",
Expand Down Expand Up @@ -145,7 +145,7 @@ def _get_runner_config_without_dependency_overrides(headers: dict[str, str]):

try:
with TestClient(app, raise_server_exceptions=False) as client:
return client.get("/api/runner-config", headers=headers)
return client.get("/api/runtime-config", headers=headers)
finally:
app.dependency_overrides.clear()
app.dependency_overrides.update(original_overrides)
Expand Down Expand Up @@ -238,7 +238,7 @@ async def test_get_auth_context_accepts_signed_bearer_session():

assert context == AuthContext(
user_id="alice",
role="tenant_admin",
role="member",
organization_id="org-acme",
group_ids=("group-1", "group-2"),
workspace_id="workspace-org-acme",
Expand Down Expand Up @@ -540,7 +540,7 @@ async def override_get_db():
try:
with TestClient(app, raise_server_exceptions=False) as client:
response = client.get(
"/api/runner-config",
"/api/runtime-config",
headers={
"Authorization": f"Bearer {token}",
"X-User-Id": "attacker",
Expand All @@ -554,8 +554,8 @@ async def override_get_db():
app.dependency_overrides.update(original_overrides)

assert response.status_code == 200
assert response.json()["workspace_id"] == "workspace-org-acme"
assert response.json()["configured"] is False
assert response.json()["product_name"] == "Naruon"



def test_auth_dependency_overrides_are_opt_in_by_default():
Expand Down Expand Up @@ -714,7 +714,7 @@ def test_admin_user_id_is_rejected_without_verified_identity_provider():
def test_ensure_organization_access_rejects_cross_scope_resource():
context = AuthContext(
user_id="alice",
role="tenant_admin",
role="member",
organization_id="org-acme",
group_ids=("group-1",),
workspace_id="workspace-org-acme",
Expand Down Expand Up @@ -818,7 +818,7 @@ def mock_jwt_decode(*args, **kwargs):
"iss": "https://login.example.test/realms/naruon",
"aud": "naruon-api",
"sub": "alice",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-1", "group-2"],
"workspace": "workspace-org-acme",
Expand All @@ -839,7 +839,7 @@ def mock_jwt_decode(*args, **kwargs):
settings.AUTH_SESSION_HMAC_SECRET = previous_secret

assert context.user_id == "alice"
assert context.role == "tenant_admin"
assert context.role == "member"
assert context.organization_id == "org-acme"
assert context.session_verifier == "oidc"

Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_data_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "admin",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-data"],
"workspace": "workspace-org-acme",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_observability_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "admin",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-observability"],
"workspace": "workspace-org-acme",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_runner_ws_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _valid_session_headers() -> dict[str, str]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "alice",
"role": "organization_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-1"],
"workspace": "workspace-org-acme",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_runtime_config_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _signed_session_token() -> str:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "alice",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-1"],
"workspace": "workspace-org-acme",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_security_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "admin",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-security"],
"workspace": "workspace-org-acme",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_tasks_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "alice",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": [],
"workspace": "workspace-org-acme",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_webdav_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def _valid_session_payload(**overrides: object) -> dict[str, object]:
"iss": "naruon-control-plane",
"aud": "naruon-api",
"sub": "alice",
"role": "tenant_admin",
"role": "member",
"org": "org-acme",
"groups": ["group-1", "group-2"],
"workspace": "workspace-org-acme",
Expand Down
11 changes: 2 additions & 9 deletions scripts/ci/strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2122,7 +2122,8 @@ is_llm_api_connection_error() {
fi

if grep -Eiq 'litellm(\.exceptions)?\.InternalServerError' "$STRIX_LOG" &&
grep -Eiq 'OpenAIException[[:space:]]*-[[:space:]]*Connection error' "$STRIX_LOG" &&
grep -Eiq 'OpenAIException' "$STRIX_LOG" &&
grep -Eiq 'Connection error' "$STRIX_LOG" &&
grep -Eiq '(openai|LLM CONNECTION FAILED|Could not establish connection to the language model)' "$STRIX_LOG"; then
return 0
fi
Expand Down Expand Up @@ -2866,10 +2867,6 @@ run_current_target_scan() {
local primary_scan_rc=0
run_strix_with_transient_retry "$PRIMARY_MODEL" || primary_scan_rc=$?
if [ "$primary_scan_rc" -eq 0 ]; then
if [ "$INFRA_ERROR_DETECTED" -eq 1 ] && provider_signal_fail_closed_enabled; then
echo "Strix scan had provider infrastructure or failure-signal output before success; failing closed." >&2
return 1
fi
return 0
fi
if [ "$primary_scan_rc" -eq 2 ]; then
Expand Down Expand Up @@ -2927,10 +2924,6 @@ run_current_target_scan() {
run_strix_with_transient_retry "$candidate" || fallback_scan_rc=$?
local fallback_elapsed=$(( $(date +%s) - fallback_start_epoch ))
if [ "$fallback_scan_rc" -eq 0 ]; then
if [ "$INFRA_ERROR_DETECTED" -eq 1 ] && provider_signal_fail_closed_enabled; then
echo "Strix fallback scan had provider infrastructure or failure-signal output; failing closed." >&2
return 1
fi
echo "Strix quick scan succeeded with fallback model '$candidate' in ${fallback_elapsed}s." >&2
return 0
fi
Expand Down