Skip to content

Conversation

@jsell-rh
Copy link
Collaborator

@jsell-rh jsell-rh commented Feb 10, 2026

Summary by CodeRabbit

  • New Features

    • Resolve tenant from X-Tenant-ID with validation, permission checks, and clear 400/403/500 behaviors.
    • Single-tenant mode (default) auto-selects a default tenant when header is missing and can auto-add users as admin/member based on a bootstrap list.
    • New settings to control single-tenant mode and bootstrap admin usernames.
  • Observability

    • Structured probes/logging for resolution outcomes and failures.
  • Tests

    • Extensive unit tests covering resolution, auto-add, error paths, and probe emissions.
  • Documentation

    • Environment variable docs updated for new IAM settings.

…lembic

- Add SQLAlchemy 2.0 with asyncpg for async database operations
- Add Alembic for schema migrations
- Add python-ulid for ULID support instead of UUID
- Create read/write engine separation with connection pooling
- Create FastAPI dependency injection for database sessions
- Create SQLAlchemy declarative base with timestamp mixin
- Initialize Alembic with async migration support
- Create initial migration for teams table (ULID primary key)
- Add comprehensive unit tests for engines and dependencies
- Configure Alembic to use settings module for database URL
- Enable ruff post-write hook for migration formatting

Refs: AIHCM-121
- Add authzed library for SpiceDB integration
- Add python-ulid for ULID support
- Create ResourceType, RelationType, Permission enums (using Group not Team)
- Create AuthorizationProvider protocol for swappable implementations
- Implement SpiceDBClient with async methods for relationships and permissions
- Create SpiceDB schema (.zed) with Tenant→Workspace→Group hierarchy
- Create AuthorizationProbe for domain-oriented observability
- Move ObservationContext to shared_kernel (fix architectural boundary)
- Add 35 unit tests for types and probes
- All 410 tests passing

Refs: AIHCM-122
Resolved conflicts in authorization files by accepting remote changes:
- shared_kernel/authorization/types.py (docstring fix)
- shared_kernel/authorization/spicedb/client.py (_parse_reference helper)
jsell-rh and others added 14 commits February 9, 2026 15:30
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…dd route documentation

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…th TDD

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Fix workspace definition in schema.zed and ConfigMap to match the
relationships actually created by the IAM outbox translator.

Schema changes (workspace definition):
- Add `relation tenant: tenant` for organizational ownership
- Change `relation parent: tenant` to `relation parent: workspace` for hierarchy
- Rename `owner` to `admin` for consistency with tenant/group definitions
- Rename `permission delete` to `permission manage` for consistency
- Add Phase 3 comments for member/permission usage

ConfigMap changes (full sync with schema.zed):
- Apply all workspace definition fixes above
- Add missing `relation member: user` to tenant definition
- Fix tenant `permission view = admin` to `permission view = admin + member`
- Add missing `permission administrate = admin` to tenant definition
- Add missing `api_key` definition (was in schema.zed but not ConfigMap)
- Add future resource type comments

Inconsistencies found and documented:
1. Schema had `relation parent: tenant` but translator writes
   `workspace#tenant@tenant` (relation name 'tenant') and
   `workspace#parent@workspace` (parent type 'workspace')
2. ConfigMap was missing tenant `member` relation, `administrate`
   permission, and entire `api_key` definition
3. RelationType.WORKSPACE enum exists but is unused by any translator
4. Permission.DELETE enum value corresponds to removed `permission delete`
   in workspace; may need cleanup in Phase 3
5. Schema `owner` relation on workspace renamed to `admin` to align with
   tenant and group naming conventions

All 970 unit tests pass (3 pre-existing SSL failures unrelated).

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Remove RelationType.WORKSPACE and Permission.DELETE which have no usage
in the codebase. Neither value corresponds to any relation or permission
in the current SpiceDB schema. They can be re-added when future resource
types (knowledge_graph, data_source) are implemented.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…tion

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…nshift-hyperfleet/kartograph into jsell/feat/AIHCM-146-workspace-authz
Add a FastAPI dependency that resolves tenant context from the X-Tenant-ID
request header, replacing the walking skeleton's get_default_tenant_id()
global function.

Key behaviors:
- Validates X-Tenant-ID header as ULID format (400 on invalid)
- Checks SpiceDB check_permission on tenant:{id}#view (403 on denied)
- In single-tenant mode (KARTOGRAPH_IAM_SINGLE_TENANT_MODE=true, default),
  auto-selects default tenant when header is missing
- In multi-tenant mode, returns 400 when header is missing

Architecture:
- TenantContext value object and TenantContextProbe in shared_kernel
  (framework-agnostic, respects architectural boundaries)
- resolve_tenant_context logic in iam/dependencies (uses FastAPI + IAM types)
- Domain-Oriented Observability probe following existing patterns
- Default tenant name sourced from IAMSettings.default_tenant_name
  (no hardcoded constants)

Refs: AIHCM-149

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Walkthrough

Adds a FastAPI dependency that resolves TenantContext from the X-Tenant-ID header with ULID validation, authorization (VIEW) checks via an AuthorizationProvider, and observability probes. When the header is missing, single-tenant mode resolves a default tenant and may auto-add the user (ADMIN if username is in bootstrap_admin_usernames, otherwise MEMBER) and persists changes; multi-tenant mode returns 400. Introduces TenantContext value object, TenantContextProbe protocol and DefaultTenantContextProbe, new IAMSettings flags, and unit tests covering validation, authz, and provisioning flows.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Dependency as Tenant Context Dependency
    participant AuthZ as Authorization Provider
    participant Repo as Tenant Repository
    participant Probe as TenantContextProbe

    Client->>Dependency: Request (X-Tenant-ID?, user_id, username)

    alt Header Present
        Dependency->>Dependency: _validate_ulid(raw)
        alt Invalid ULID
            Dependency->>Probe: invalid_tenant_id_format(raw, user)
            Dependency-->>Client: HTTP 400
        else Valid ULID
            Dependency->>AuthZ: check VIEW permission on tenant
            alt Authorization Granted
                Dependency->>Probe: tenant_resolved_from_header(tenant, user)
                Dependency-->>Client: TenantContext(source: "header")
            else Authorization Denied
                Dependency->>Probe: tenant_access_denied(tenant, user)
                Dependency-->>Client: HTTP 403
            else AuthZ Error
                Dependency->>Probe: tenant_authz_check_failed(tenant, user, err)
                Dependency-->>Client: HTTP 403
            end
        end
    else Header Missing
        Dependency->>Probe: tenant_header_missing(user)
        alt Single-Tenant Mode
            Dependency->>Repo: find_by_name(default_tenant_name)
            alt Tenant Found
                Dependency->>AuthZ: check VIEW permission on tenant
                alt Permission Granted
                    Dependency->>Probe: tenant_resolved_from_default(tenant, user)
                    Dependency-->>Client: TenantContext(source: "default")
                else Not a Member
                    Dependency->>Dependency: auto-add user (ADMIN if bootstrap else MEMBER)
                    Dependency->>Repo: save tenant (persist)
                    Dependency->>Probe: user_auto_added_as_admin/member(...)
                    Dependency-->>Client: TenantContext(source: "default")
                end
            else Tenant Not Found
                Dependency->>Probe: default_tenant_not_found()
                Dependency-->>Client: HTTP 500
            end
        else Multi-Tenant Mode
            Dependency-->>Client: HTTP 400
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title references a Jira ticket (AIHCM-149) but lacks clarity about the actual feature; 'tenant context dependency' is moderately descriptive but the ticket reference adds noise for developers scanning history. Consider clarifying the title to better describe the feature purpose, such as 'feat(api.iam): add tenant context dependency resolver' or removing the ticket reference for improved readability.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 98.86% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jsell/feat/AIHCM-149-tenant-context-dependency

Comment @coderabbitai help to get the list of available commands and usage tips.

@jsell-rh jsell-rh self-assigned this Feb 10, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-02-11 16:44 UTC

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/api/iam/dependencies/tenant_context.py`:
- Around line 88-96: When x_tenant_id is None and single_tenant_mode is true the
code returns _resolve_default_tenant without verifying the caller's membership;
change this so that before calling _resolve_default_tenant you perform the same
membership/permission check used for explicit tenant IDs (using user_id and
tenant_repository) and only return the default if the check passes, otherwise
raise an authorization error, or alternatively make single_tenant_mode require
an explicit dev-only flag and hard-fail when that flag is unset; update the
logic around x_tenant_id, single_tenant_mode and the call site of
_resolve_default_tenant (and any helpers that validate membership) accordingly.
🧹 Nitpick comments (2)
src/api/tests/unit/shared_kernel/middleware/test_tenant_context.py (1)

62-73: Protocol conformance test could be strengthened.

The test verifies methods are callable but doesn't leverage Python's typing.runtime_checkable for protocol verification. Consider using isinstance check if TenantContextProbe is decorated with @runtime_checkable.

However, this is a minor observation—the current approach is pragmatic and tests functional behavior.

💡 Optional enhancement
from typing import runtime_checkable

# In tenant_context_probe.py, if not already:
`@runtime_checkable`
class TenantContextProbe(Protocol):
    ...

# Then in test:
def test_implements_protocol(self) -> None:
    """DefaultTenantContextProbe should implement TenantContextProbe protocol."""
    from shared_kernel.middleware.observability.tenant_context_probe import TenantContextProbe
    probe = DefaultTenantContextProbe()
    assert isinstance(probe, TenantContextProbe)
src/api/shared_kernel/middleware/tenant_context.py (1)

16-30: Clean value object design.

The frozen dataclass is appropriate for an immutable value object. Consider using Literal["header", "default"] for the source field to provide stronger type safety and IDE autocompletion.

💡 Optional type narrowing
 from dataclasses import dataclass
+from typing import Literal
+
+TenantSource = Literal["header", "default"]
 
 
 `@dataclass`(frozen=True)
 class TenantContext:
     """..."""
 
     tenant_id: str
-    source: str
+    source: TenantSource

Comment on lines 88 to 96
# Case 1: Header is missing
if x_tenant_id is None:
if single_tenant_mode:
return await _resolve_default_tenant(
user_id=user_id,
probe=probe,
tenant_repository=tenant_repository,
default_tenant_name=default_tenant_name,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential authz bypass when default tenant is auto-selected.
In single-tenant mode, a missing header returns the default tenant without checking membership. If this mode is ever enabled outside dev, a user can omit the header to bypass tenant access enforcement. Consider applying the same permission check before returning the default tenant (or explicitly hard-failing when single_tenant_mode is enabled in non-dev).

🔐 Suggested fix (apply authz check before returning default tenant)
         if single_tenant_mode:
-            return await _resolve_default_tenant(
+            tenant_ctx = await _resolve_default_tenant(
                 user_id=user_id,
                 probe=probe,
                 tenant_repository=tenant_repository,
                 default_tenant_name=default_tenant_name,
             )
+            try:
+                has_permission = await authz.check_permission(
+                    resource=format_resource(ResourceType.TENANT, tenant_ctx.tenant_id),
+                    permission=Permission.VIEW,
+                    subject=format_subject(ResourceType.USER, user_id),
+                )
+            except Exception as e:
+                probe.tenant_authz_check_failed(
+                    tenant_id=tenant_ctx.tenant_id,
+                    user_id=user_id,
+                    error=e,
+                )
+                raise
+
+            if not has_permission:
+                probe.tenant_access_denied(
+                    tenant_id=tenant_ctx.tenant_id,
+                    user_id=user_id,
+                )
+                raise HTTPException(
+                    status_code=status.HTTP_403_FORBIDDEN,
+                    detail="You do not have access to this tenant",
+                )
+
+            return tenant_ctx
🤖 Prompt for AI Agents
In `@src/api/iam/dependencies/tenant_context.py` around lines 88 - 96, When
x_tenant_id is None and single_tenant_mode is true the code returns
_resolve_default_tenant without verifying the caller's membership; change this
so that before calling _resolve_default_tenant you perform the same
membership/permission check used for explicit tenant IDs (using user_id and
tenant_repository) and only return the default if the check passes, otherwise
raise an authorization error, or alternatively make single_tenant_mode require
an explicit dev-only flag and hard-fail when that flag is unset; update the
logic around x_tenant_id, single_tenant_mode and the call site of
_resolve_default_tenant (and any helpers that validate membership) accordingly.

jsell-rh and others added 2 commits February 10, 2026 17:00
Add automatic user provisioning when resolving the default tenant context
in single-tenant mode. Users without existing tenant access are auto-added
as MEMBER (or ADMIN if their username is in the bootstrap list).

- Change TenantContext.source type from str to Literal["header", "default"]
- Add bootstrap_admin_usernames setting to IAMSettings
- Add user_auto_added_as_member/admin and user_auto_add_failed probes
- Update _resolve_default_tenant to check SpiceDB and auto-add users

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…ub.com:openshift-hyperfleet/kartograph into jsell/feat/AIHCM-149-tenant-context-dependency
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/api/iam/dependencies/tenant_context.py`:
- Around line 201-207: The default-tenant path calls authz.check_permission
(with resource=format_resource(ResourceType.TENANT, tenant.id.value),
permission=Permission.VIEW, subject=format_subject(ResourceType.USER, user_id))
but currently lets exceptions escape without emitting a domain probe; wrap this
authz.check_permission call in a try/except that on exception emits the
tenant_authz_check_failed probe (including relevant context: tenant.id.value and
user_id) and then re-raises the original exception to preserve behavior.

jsell-rh and others added 2 commits February 11, 2026 10:45
Add automatic user provisioning when resolving the default tenant context
in single-tenant mode. Users without existing tenant access are auto-added
as MEMBER (or ADMIN if their username is in the bootstrap list).

- Change TenantContext.source type from str to Literal["header", "default"]
- Add bootstrap_admin_usernames setting to IAMSettings
- Add user_auto_added_as_member/admin and user_auto_add_failed probes
- Update _resolve_default_tenant to check SpiceDB and auto-add users
- Wrap default-tenant authz check in try/except with probe emission

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…ub.com:openshift-hyperfleet/kartograph into jsell/feat/AIHCM-149-tenant-context-dependency
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@env/api.env`:
- Line 12: Remove the hard-coded bootstrap admin username by changing the
default value of KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES from '["alice"]' to an
empty list (e.g., '[]' or an empty string as your env convention) so no admin is
implicitly granted; ensure any required local or deployment secrets explicitly
set KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES to the intended usernames instead
of relying on the default.

In `@src/api/iam/dependencies/tenant_context.py`:
- Around line 56-83: In get_tenant_context validate and normalize the incoming
user_id and username immediately (strip whitespace), and if either is empty
after normalization raise an appropriate error (e.g., HTTPException/ValueError)
before performing any authz calls or auto-add logic; ensure downstream uses
(SpiceDB subject creation, authz checks in authz, and membership persistence for
bootstrap_admin_usernames) only occur with the cleaned non-empty values so
invalid/whitespace inputs cannot produce malformed subjects or membership
records.
- Around line 40-53: The _validate_ulid function currently parses the raw_value
with ULID.from_str but returns the original raw string, preserving non-canonical
casing; change it to parse the ULID (ULID.from_str(raw_value)), then use the
canonical string form via str(parsed_ulid) when constructing and returning the
TenantId (i.e., TenantId(value=str(parsed_ulid))) so TenantId.value is the
normalized ULID string compatible with SpiceDB keys.

SPICEDB_ENDPOINT="spicedb:50051"
SPICEDB_PRESHARED_KEY="changeme"
KARTOGRAPH_CORS_ORIGINS=["*"]
KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES='["alice"]' No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid committing a default bootstrap admin username.

Line 12 hard-codes alice as a bootstrap admin, which can unintentionally grant admin rights in single-tenant mode. Prefer an empty default and let deployments/local dev override explicitly.

🔧 Suggested safer default
-KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES='["alice"]'
+KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES='[]'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES='["alice"]'
KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES='[]'
🤖 Prompt for AI Agents
In `@env/api.env` at line 12, Remove the hard-coded bootstrap admin username by
changing the default value of KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES from
'["alice"]' to an empty list (e.g., '[]' or an empty string as your env
convention) so no admin is implicitly granted; ensure any required local or
deployment secrets explicitly set KARTOGRAPH_IAM_BOOTSTRAP_ADMIN_USERNAMES to
the intended usernames instead of relying on the default.

jsell-rh and others added 2 commits February 11, 2026 11:15
Add automatic user provisioning when resolving the default tenant context
in single-tenant mode. Users without existing tenant access are auto-added
as MEMBER (or ADMIN if their username is in the bootstrap list).

- Change TenantContext.source type from str to Literal["header", "default"]
- Add bootstrap_admin_usernames setting to IAMSettings
- Add user_auto_added_as_member/admin and user_auto_add_failed probes
- Update _resolve_default_tenant to check SpiceDB and auto-add users
- Wrap default-tenant authz check in try/except with probe emission
- Normalize ULID to canonical uppercase form for consistent SpiceDB keys
- Validate and strip user_id and username inputs at entry point

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…ub.com:openshift-hyperfleet/kartograph into jsell/feat/AIHCM-149-tenant-context-dependency
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/api/iam/dependencies/tenant_context.py`:
- Around line 237-241: The username membership check against
bootstrap_admin_usernames is currently case-sensitive (role assignment logic in
TenantRole selection using username and bootstrap_admin_usernames), causing
mismatches if casing differs; update the comparison to normalize both sides
(e.g., lower()/casefold()) before membership testing or document that
bootstrap_admin_usernames must match identity provider casing exactly — modify
the logic around the role assignment where username is compared to
bootstrap_admin_usernames so it uses a case-insensitive comparison (normalize
username and each configured admin entry) to determine whether to assign
TenantRole.ADMIN.
🧹 Nitpick comments (1)
src/api/iam/dependencies/tenant_context.py (1)

55-56: Redundant .upper() call before ULID parsing.

ULID.from_str() is already case-insensitive per Crockford's Base32 spec, and str(parsed_ulid) returns the canonical uppercase form. The explicit .upper() call is unnecessary.

♻️ Suggested simplification
-    parsed_ulid = ULID.from_str(raw_value.upper())
+    parsed_ulid = ULID.from_str(raw_value)
     return TenantId(value=str(parsed_ulid))

Comment on lines +237 to +241
role = (
TenantRole.ADMIN
if username in bootstrap_admin_usernames
else TenantRole.MEMBER
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how bootstrap_admin_usernames is configured and documented
rg -n "bootstrap_admin" --type=py -C 3

Repository: openshift-hyperfleet/kartograph

Length of output: 30808


🏁 Script executed:

#!/bin/bash
# Find where username comes from in the _resolve_default_tenant function
cat -n src/api/iam/dependencies/tenant_context.py | sed -n '180,250p'

Repository: openshift-hyperfleet/kartograph

Length of output: 3121


🏁 Script executed:

#!/bin/bash
# Search for case normalization patterns with username in the codebase
rg -n "lower\(\)|upper\(\)" src/api/iam/ --type=py | head -30

Repository: openshift-hyperfleet/kartograph

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Check how username is extracted/normalized at the entry point
rg -n "def get_tenant_context" src/api/iam/dependencies/tenant_context.py -A 30

Repository: openshift-hyperfleet/kartograph

Length of output: 1433


🏁 Script executed:

#!/bin/bash
# Find where get_tenant_context is registered/used as a dependency
rg -n "get_tenant_context" src/api/iam/ --type=py -B 2 -A 2

Repository: openshift-hyperfleet/kartograph

Length of output: 797


🏁 Script executed:

#!/bin/bash
# Check where the username dependency comes from
rg -n "def.*username" src/api/iam/dependencies/ --type=py -A 5

Repository: openshift-hyperfleet/kartograph

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Search for dependencies that provide username
rg -n "username.*:" src/api/iam/ --type=py -B 3 | head -60

Repository: openshift-hyperfleet/kartograph

Length of output: 4033


🏁 Script executed:

#!/bin/bash
# Look for auth-related dependencies or extraction
rg -n "Depends\(" src/api/iam/dependencies/ --type=py -A 1 -B 1

Repository: openshift-hyperfleet/kartograph

Length of output: 7066


🏁 Script executed:

#!/bin/bash
# Find where username is extracted from CurrentUser
rg -n "current_user" src/api/iam/dependencies/tenant_context.py --type=py -B 2 -A 2

Repository: openshift-hyperfleet/kartograph

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check the full get_tenant_context function to see all dependencies
cat -n src/api/iam/dependencies/tenant_context.py | sed -n '59,130p'

Repository: openshift-hyperfleet/kartograph

Length of output: 3261


🏁 Script executed:

#!/bin/bash
# Look for where get_tenant_context dependencies are resolved (find the app setup)
rg -n "app.dependency_overrides\|override_get_tenant_context\|add_api_route" src/api/ --type=py | head -20

Repository: openshift-hyperfleet/kartograph

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check if there's an __init__ or main.py that sets up dependencies
find src/api -name "__init__.py" -o -name "main.py" | xargs grep -l "get_tenant_context\|dependency" 2>/dev/null

Repository: openshift-hyperfleet/kartograph

Length of output: 181


🏁 Script executed:

#!/antml:
# Search for CurrentUser usage to understand where username comes from
rg -n "get_current_user" src/api/iam/dependencies/ --type=py -B 2 -A 5

Repository: openshift-hyperfleet/kartograph

Length of output: 5626


🏁 Script executed:

#!/bin/bash
# Look for where user_id and username are resolved as dependencies in tenant_context
cat -n src/api/iam/dependencies/tenant_context.py | head -70

Repository: openshift-hyperfleet/kartograph

Length of output: 2776


🏁 Script executed:

#!/bin/bash
# Search for where user_id and username dependency functions are defined
rg -n "^def user_id|^def username|^async def user_id|^async def username" src/api/ --type=py -A 3

Repository: openshift-hyperfleet/kartograph

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check imports and look for where user_id and username are used as dependencies
rg -n "Depends\|user_id\|username" src/api/iam/dependencies/tenant_context.py | head -50

Repository: openshift-hyperfleet/kartograph

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Check infrastructure and settings for how dependencies are configured
find src/api -name "*.py" -type f | xargs grep -l "user_id.*Depends\|username.*Depends" 2>/dev/null

Repository: openshift-hyperfleet/kartograph

Length of output: 59


🏁 Script executed:

#!/antml:
# Search for FastAPI app setup or routes that use get_tenant_context
find src/api -name "*.py" -type f | xargs grep -l "get_tenant_context" 2>/dev/null | head -10

Repository: openshift-hyperfleet/kartograph

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Check test file to see how get_tenant_context is being called
cat -n src/api/tests/unit/iam/test_tenant_context_dependency.py | sed -n '1,100p'

Repository: openshift-hyperfleet/kartograph

Length of output: 4271


🏁 Script executed:

#!/bin/bash
# Search the test file for how username is being tested/passed
rg -n "username" src/api/tests/unit/iam/test_tenant_context_dependency.py | head -40

Repository: openshift-hyperfleet/kartograph

Length of output: 2008


🏁 Script executed:

#!/bin/bash
# Look at the test for auto-add user as ADMIN when in bootstrap list
cat -n src/api/tests/unit/iam/test_tenant_context_dependency.py | sed -n '960,1000p'

Repository: openshift-hyperfleet/kartograph

Length of output: 1845


🏁 Script executed:

#!/bin/bash
# Search for any test cases checking case sensitivity of usernames
rg -n "case|Case|CASE|lower\(\)|upper\(\)" src/api/tests/unit/iam/test_tenant_context_dependency.py

Repository: openshift-hyperfleet/kartograph

Length of output: 1067


🏁 Script executed:

#!/bin/bash
# Check if there are any docs or comments about username case handling
rg -n "case.*sensitive|case.*insensitive|username.*case" src/api/ --type=py -i

Repository: openshift-hyperfleet/kartograph

Length of output: 1854


🏁 Script executed:

#!/antml:
# Look at how identity provider usernames might be provided
rg -n "oauth\|saml\|ldap\|jwt\|claims" src/api/iam/ --type=py -i | head -20

Repository: openshift-hyperfleet/kartograph

Length of output: 57


Case-sensitive username comparison for bootstrap admins.

The check at line 239 username in bootstrap_admin_usernames is case-sensitive. If the identity provider returns "Alice" but the configuration contains "alice", the user won't be granted the admin role. This differs from other identifiers in the codebase (like ULID) which are normalized case-insensitively. Consider whether case-insensitive comparison is intended, or add documentation clarifying that bootstrap admin usernames must match the identity provider's case exactly.

🔧 Suggested fix for case-insensitive matching
+        username_lower = username.lower()
+        bootstrap_lower = [u.lower() for u in bootstrap_admin_usernames]
         role = (
             TenantRole.ADMIN
-            if username in bootstrap_admin_usernames
+            if username_lower in bootstrap_lower
             else TenantRole.MEMBER
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
role = (
TenantRole.ADMIN
if username in bootstrap_admin_usernames
else TenantRole.MEMBER
)
username_lower = username.lower()
bootstrap_lower = [u.lower() for u in bootstrap_admin_usernames]
role = (
TenantRole.ADMIN
if username_lower in bootstrap_lower
else TenantRole.MEMBER
)
🤖 Prompt for AI Agents
In `@src/api/iam/dependencies/tenant_context.py` around lines 237 - 241, The
username membership check against bootstrap_admin_usernames is currently
case-sensitive (role assignment logic in TenantRole selection using username and
bootstrap_admin_usernames), causing mismatches if casing differs; update the
comparison to normalize both sides (e.g., lower()/casefold()) before membership
testing or document that bootstrap_admin_usernames must match identity provider
casing exactly — modify the logic around the role assignment where username is
compared to bootstrap_admin_usernames so it uses a case-insensitive comparison
(normalize username and each configured admin entry) to determine whether to
assign TenantRole.ADMIN.

@jsell-rh jsell-rh merged commit bfa46d1 into main Feb 11, 2026
12 checks passed
@jsell-rh jsell-rh deleted the jsell/feat/AIHCM-149-tenant-context-dependency branch February 11, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant