-
Notifications
You must be signed in to change notification settings - Fork 1
feat(api.iam): aihcm 149 tenant context dependency #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(api.iam): aihcm 149 tenant context dependency #213
Conversation
…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)
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>
WalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this 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_checkablefor protocol verification. Consider usingisinstancecheck ifTenantContextProbeis 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 thesourcefield 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
| # 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, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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
There was a problem hiding this 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.
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
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
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
There was a problem hiding this 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, andstr(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))
| role = ( | ||
| TenantRole.ADMIN | ||
| if username in bootstrap_admin_usernames | ||
| else TenantRole.MEMBER | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how bootstrap_admin_usernames is configured and documented
rg -n "bootstrap_admin" --type=py -C 3Repository: 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 -30Repository: 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 30Repository: 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 2Repository: 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 5Repository: 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 -60Repository: 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 1Repository: 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 2Repository: 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 -20Repository: 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/nullRepository: 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 5Repository: 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 -70Repository: 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 3Repository: 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 -50Repository: 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/nullRepository: 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 -10Repository: 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 -40Repository: 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.pyRepository: 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 -iRepository: 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 -20Repository: 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.
| 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.
Summary by CodeRabbit
New Features
Observability
Tests
Documentation