Skip to content

BED-8788: add PAT-backed enterprise SSO collection and fix repeated enterprise SSO pagination#22

Open
jaredcatkinson wants to merge 14 commits into
SpecterOps:mainfrom
jaredcatkinson:fix/BED-8788-enterprise-sso-pat
Open

BED-8788: add PAT-backed enterprise SSO collection and fix repeated enterprise SSO pagination#22
jaredcatkinson wants to merge 14 commits into
SpecterOps:mainfrom
jaredcatkinson:fix/BED-8788-enterprise-sso-pat

Conversation

@jaredcatkinson

@jaredcatkinson jaredcatkinson commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
  • add optional pat_token support for enterprise app sources
  • create a dedicated SSO client/context when a PAT is provided
  • gate enterprise SSO collections on the presence of that client
  • run enterprise SAML provider and external identity collection with the PAT client
  • fix enterprise resource iteration so SSO collection does not repeat per org page
  • give PAT-backed requests the same retry/backoff behavior as the app client
  • add enterprise resource coverage tests

Summary by CodeRabbit

  • New Features
    • Added environment-scoped SAML/SSO modeling when a PAT is provided, including normalized SAML service provider, trusted issuer, and assertion consumer service nodes plus corresponding mappings.
    • Introduced environment branch-policy modeling and related deployment/approval edges.
  • Bug Fixes
    • More robust handling of missing or partial SSO/SAML data to avoid noisy or failed runs.
  • Improvements
    • Enterprise organization pagination and environment-scoped identity outputs were improved; repository graph traversal semantics were standardized to use GH_Contains for contained resources.
    • Refined environment secrets/variables and edge documentation; enhanced environment protection fields and branch-pattern behavior.
  • Tests
    • Added/expanded unit tests for enterprise retrieval/pagination, SAML helpers/models, environment protection/policy logic, and workflow edge generation.

…nterprise SSO pagination

- add optional pat_token support for enterprise app sources
- create a dedicated SSO client/context when a PAT is provided
- gate enterprise SSO collections on the presence of that client
- run enterprise SAML provider and external identity collection with the PAT client
- fix enterprise resource iteration so SSO collection does not repeat per org page
- give PAT-backed requests the same retry/backoff behavior as the app client
- add enterprise resource coverage tests
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds PAT-backed SSO, environment-scoped SAML graph assets and identity mapping, environment deployment relationships, workflow containment queries, empty-input table initialization, and related tests and documentation.

Changes

Enterprise SAML and graph integration

Layer / File(s) Summary
SSO configuration and resource flow
src/openhound_github/helpers.py, src/openhound_github/source.py, src/openhound_github/graphql.py, src/openhound_github/resources/*, tests/test_enterprise_resources.py
Adds PAT-backed SSO setup, revised GraphQL queries, enterprise and organization resource orchestration, SSO-gated SAML resources, and resource tests.
Normalized SAML graph assets
src/openhound_github/kinds/*, src/openhound_github/models/saml_*.py, src/openhound_github/models/saml_provider.py, src/openhound_github/models/__init__.py
Adds SAML node and edge kinds, shared helpers, service provider, issuer, and ACS assets, and environment-scoped provider relationships.
Environment-based identity mapping
src/openhound_github/models/external_identity.py, src/openhound_github/lookup.py, tests/test_external_identity_tenant_matching.py, tests/test_saml_models.py
Updates identity inputs, environment IdP lookup, foreign-provider matching, SAML account edges, deployment-aware identifiers, and identity validation.
Environment deployment and access graph
src/openhound_github/models/environment*.py, src/openhound_github/models/env_*.py, src/openhound_github/models/org_variable.py, src/openhound_github/models/repository_role.py, extension/schema.json, descriptions/*
Adds environment protection properties, branch-policy nodes, deployment eligibility, reviewer approvals, environment-scoped secret and variable access, environment creation/edit edges, and matching documentation.
Workflow containment and integration support
src/openhound_github/models/workflow*.py, src/openhound_github/models/repository.py, src/openhound_github/transforms.py, src/openhound_github/extension.yaml, extension/saved_searches/*, tests/*
Replaces hierarchy relationships with GH_Contains, adds workflow query metadata and environment references, initializes optional DuckDB tables, updates saved searches, adds extension metadata, and expands model tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Source as enterprise_app
  participant Resources as enterprise_resources
  participant SSO as sso_client
  participant API as GraphQL API
  participant Models as SAML and environment graph assets
  Source->>Resources: provide optional PAT-backed sso_client
  Resources->>SSO: request EnterpriseSAMLProvider
  SSO->>API: send GraphQL request
  API-->>SSO: return provider data
  SSO-->>Resources: yield environment-scoped provider
  Resources->>Models: build SAML assets and deployment relationships
Loading

Poem

A rabbit hops through SAML’s bright stream,
PAT keys unlock a GraphQL dream,
Environments branch and deploy,
New graph edges leap with joy,
While tests keep watch on every beam! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: PAT-backed enterprise SSO collection and the enterprise SSO pagination fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
tests/test_enterprise_resources.py (4)

55-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Strengthen the post-call assertion.

Only the call count is checked; the actual GraphQL variables (e.g., enterprise_name) sent in the post body aren't verified. A regression that sends the wrong enterprise slug/name would pass this test silently.

Suggested strengthening
     assert len(rows) == 1
     assert rows[0].id == "E_1"
     assert len(client.post_calls) == 1
+    _, posted_json = client.post_calls[0]
+    assert posted_json["variables"]["slug"] == "acme"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_enterprise_resources.py` at line 55, Strengthen the enterprise
resources test by verifying the actual GraphQL payload, not just the number of
post calls. In the test around client.post_calls, assert the posted
body/variables include the expected enterprise_name (and any other key GraphQL
variables) so a wrong slug/name in the request will fail. Use the existing
client.post_calls structure in tests/test_enterprise_resources.py to inspect the
single post call and validate its variables.

89-93: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Also assert the pagination call arguments.

Only len(client.paginate_calls) == 1 is checked (line 93); the kwargs passed to paginate (query, variables including the enterprise node id/cursor) aren't verified. Given the PR's stated fix is specifically about correct enterprise resource iteration/pagination variables, asserting on client.paginate_calls[0][1] would give stronger regression protection for the exact bug this PR fixes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_enterprise_resources.py` around lines 89 - 93, The test for
enterprise resource iteration only checks that paginate was called once, but it
does not verify the query or variables used. Update the test around the paginate
call in the enterprise resources flow to assert the captured arguments from
client.paginate_calls[0][1], including the enterprise node id and cursor-related
variables, so the regression coverage matches the pagination fix.

87-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reliance on __wrapped__ couples the test to decorator internals.

Accessing enterprise_organizations.__wrapped__ assumes the resource/transformer decorator preserves the original callable via functools.wraps (or equivalent). If the decorator implementation changes (e.g., wraps differently or omits __wrapped__), this test breaks with an unrelated AttributeError rather than a meaningful assertion failure. Consider invoking the resource through its public/documented test surface if the underlying decorator (likely dlt's @dlt.transformer) exposes one, or add a comment explaining why __wrapped__ is required here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_enterprise_resources.py` at line 87, The test is reaching into
`enterprise_organizations.__wrapped__`, which ties it to decorator internals and
can fail with an `AttributeError` if the wrapper implementation changes. Update
the test to use the public/documented way to invoke the
`enterprise_organizations` resource/transformer in
`tests/test_enterprise_resources.py`, or add a short justification comment if
calling the underlying callable directly is truly required. Keep the assertion
focused on the emitted rows rather than the decorator behavior.

58-93: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Missing coverage for the SSO-gated resources.

The PR's core change is gating enterprise_saml_provider/enterprise_external_identities on sso_client presence, but this test file only covers enterprise and enterprise_organizations. No test verifies the skip/warning behavior when sso_client is absent, nor the happy path when it is present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_enterprise_resources.py` around lines 58 - 93, Add test coverage
for the SSO-gated enterprise resources so the new gating behavior is verified.
In the enterprise resources test module, add one case for the absence of
sso_client that confirms enterprise_saml_provider and
enterprise_external_identities are skipped or warn as intended, and another case
for the present sso_client path that confirms both resources execute normally
through the enterprise_saml_provider and enterprise_external_identities entry
points. Use the existing SourceContext, _FakeClient, and enterprise_data setup
patterns to keep the tests aligned with the current enterprise_organizations
coverage.
src/openhound_github/graphql.py (1)

169-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate field list between the two SAML queries.

ENTERPRISE_SAML_PROVIDER_QUERY's samlIdentityProvider field selection is byte-for-byte duplicated from ENTERPRISE_SAML_QUERY (Lines 125-131). Consider extracting a shared GraphQL fragment for the provider fields to keep both queries in sync as the schema evolves.

♻️ Example using a shared fragment
fragment SamlIdentityProviderFields on SamlIdentityProvider {
    id
    issuer
    ssoUrl
    digestMethod
    signatureMethod
    idpCertificate
}

Then reference ...SamlIdentityProviderFields in both queries instead of repeating the field list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/graphql.py` around lines 169 - 188, The SAML provider
field selection is duplicated between ENTERPRISE_SAML_QUERY and
ENTERPRISE_SAML_PROVIDER_QUERY, so extract the repeated samlIdentityProvider
fields into a shared GraphQL fragment and reuse it in both query strings. Update
the query definitions in graphql.py to reference the new fragment from the
existing ENTERPRISE_SAML_QUERY and ENTERPRISE_SAML_PROVIDER_QUERY symbols so
future schema changes only need one edit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhound_github/resources/enterprise.py`:
- Around line 70-96: Add the same graceful error handling used in enterprise()
around the new enterprise_organizations GraphQL pagination flow. Wrap the
ctx.client.paginate("/graphql", ...) loop in try/except (including HTTPError or
a broader request exception if consistent with the module), log the failure with
enough context, and return/stop cleanly instead of letting the exception escape.
Use the enterprise_organizations paginator setup and the ctx.client.paginate
call as the key locations to update.
- Around line 345-372: Add error handling around the SSO GraphQL request in
enterprise_saml_provider: the unguarded client.post("/graphql",
json=data).json() call can raise on non-2xx or network failures and should
degrade gracefully like enterprise(). Wrap the POST/JSON fetch in a try/except,
log a warning or error with ctx.enterprise_name and the exception details, then
return early when the SSO-backed lookup fails; keep the existing saml_provider
parsing and yield logic unchanged when the request succeeds.

---

Nitpick comments:
In `@src/openhound_github/graphql.py`:
- Around line 169-188: The SAML provider field selection is duplicated between
ENTERPRISE_SAML_QUERY and ENTERPRISE_SAML_PROVIDER_QUERY, so extract the
repeated samlIdentityProvider fields into a shared GraphQL fragment and reuse it
in both query strings. Update the query definitions in graphql.py to reference
the new fragment from the existing ENTERPRISE_SAML_QUERY and
ENTERPRISE_SAML_PROVIDER_QUERY symbols so future schema changes only need one
edit.

In `@tests/test_enterprise_resources.py`:
- Line 55: Strengthen the enterprise resources test by verifying the actual
GraphQL payload, not just the number of post calls. In the test around
client.post_calls, assert the posted body/variables include the expected
enterprise_name (and any other key GraphQL variables) so a wrong slug/name in
the request will fail. Use the existing client.post_calls structure in
tests/test_enterprise_resources.py to inspect the single post call and validate
its variables.
- Around line 89-93: The test for enterprise resource iteration only checks that
paginate was called once, but it does not verify the query or variables used.
Update the test around the paginate call in the enterprise resources flow to
assert the captured arguments from client.paginate_calls[0][1], including the
enterprise node id and cursor-related variables, so the regression coverage
matches the pagination fix.
- Line 87: The test is reaching into `enterprise_organizations.__wrapped__`,
which ties it to decorator internals and can fail with an `AttributeError` if
the wrapper implementation changes. Update the test to use the public/documented
way to invoke the `enterprise_organizations` resource/transformer in
`tests/test_enterprise_resources.py`, or add a short justification comment if
calling the underlying callable directly is truly required. Keep the assertion
focused on the emitted rows rather than the decorator behavior.
- Around line 58-93: Add test coverage for the SSO-gated enterprise resources so
the new gating behavior is verified. In the enterprise resources test module,
add one case for the absence of sso_client that confirms
enterprise_saml_provider and enterprise_external_identities are skipped or warn
as intended, and another case for the present sso_client path that confirms both
resources execute normally through the enterprise_saml_provider and
enterprise_external_identities entry points. Use the existing SourceContext,
_FakeClient, and enterprise_data setup patterns to keep the tests aligned with
the current enterprise_organizations coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b1f1ac8-1163-4a8d-9e47-6bb356ee1893

📥 Commits

Reviewing files that changed from the base of the PR and between 2d4305d and 1fd0bca.

📒 Files selected for processing (5)
  • src/openhound_github/graphql.py
  • src/openhound_github/helpers.py
  • src/openhound_github/resources/enterprise.py
  • src/openhound_github/source.py
  • tests/test_enterprise_resources.py

Comment on lines +70 to +96
paginator = GraphQLCursorPaginator(
page_info_path="data.enterprise.organizations.pageInfo",
cursor_variable="after",
cursor_field="endCursor",
has_next_field="hasNextPage",
)
data = {
"query": ENTERPRISE_QUERY,
"variables": {"slug": ctx.enterprise_name, "after": None},
}

for page_data in ctx.client.paginate(
"/graphql",
method="POST",
json=data,
paginator=paginator,
data_selector="data",
):
for enterprise_object in page_data:
es_data = enterprise_object.get("enterprise", {})
orgs = (es_data.get("organizations") or {}).get("nodes", [])
for org in orgs:
yield {
**org,
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add error handling around the new enterprise_organizations GraphQL pagination call.

Unlike enterprise() (Lines 53-62), which wraps its GraphQL POST in try/except with graceful logging, this newly-added independent pagination call has no error handling. Since client.paginate()/.post() raise HTTPError on non-2xx responses by default, a transient or permission error here will propagate as an unhandled exception and fail this resource entirely instead of degrading gracefully.

🛡️ Suggested fix
 def enterprise_organizations(enterprise_data: Enterprise, ctx: SourceContext):
     paginator = GraphQLCursorPaginator(
         page_info_path="data.enterprise.organizations.pageInfo",
         cursor_variable="after",
         cursor_field="endCursor",
         has_next_field="hasNextPage",
     )
     data = {
         "query": ENTERPRISE_QUERY,
         "variables": {"slug": ctx.enterprise_name, "after": None},
     }

-    for page_data in ctx.client.paginate(
-        "/graphql",
-        method="POST",
-        json=data,
-        paginator=paginator,
-        data_selector="data",
-    ):
-        for enterprise_object in page_data:
-            es_data = enterprise_object.get("enterprise", {})
-            orgs = (es_data.get("organizations") or {}).get("nodes", [])
-            for org in orgs:
-                yield {
-                    **org,
-                    "enterprise_node_id": enterprise_data.id,
-                    "enterprise_slug": ctx.enterprise_name,
-                }
+    try:
+        for page_data in ctx.client.paginate(
+            "/graphql",
+            method="POST",
+            json=data,
+            paginator=paginator,
+            data_selector="data",
+        ):
+            for enterprise_object in page_data:
+                es_data = enterprise_object.get("enterprise", {})
+                orgs = (es_data.get("organizations") or {}).get("nodes", [])
+                for org in orgs:
+                    yield {
+                        **org,
+                        "enterprise_node_id": enterprise_data.id,
+                        "enterprise_slug": ctx.enterprise_name,
+                    }
+    except Exception as e:
+        logger.error(
+            f"Error in resource 'enterprise_organizations' processing enterprise '{ctx.enterprise_name}': {e}",
+            extra={"resource": "enterprise_organizations", "phase": "resource_iteration"},
+        )
+        return
📝 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
paginator = GraphQLCursorPaginator(
page_info_path="data.enterprise.organizations.pageInfo",
cursor_variable="after",
cursor_field="endCursor",
has_next_field="hasNextPage",
)
data = {
"query": ENTERPRISE_QUERY,
"variables": {"slug": ctx.enterprise_name, "after": None},
}
for page_data in ctx.client.paginate(
"/graphql",
method="POST",
json=data,
paginator=paginator,
data_selector="data",
):
for enterprise_object in page_data:
es_data = enterprise_object.get("enterprise", {})
orgs = (es_data.get("organizations") or {}).get("nodes", [])
for org in orgs:
yield {
**org,
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}
paginator = GraphQLCursorPaginator(
page_info_path="data.enterprise.organizations.pageInfo",
cursor_variable="after",
cursor_field="endCursor",
has_next_field="hasNextPage",
)
data = {
"query": ENTERPRISE_QUERY,
"variables": {"slug": ctx.enterprise_name, "after": None},
}
try:
for page_data in ctx.client.paginate(
"/graphql",
method="POST",
json=data,
paginator=paginator,
data_selector="data",
):
for enterprise_object in page_data:
es_data = enterprise_object.get("enterprise", {})
orgs = (es_data.get("organizations") or {}).get("nodes", [])
for org in orgs:
yield {
**org,
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}
except Exception as e:
logger.error(
f"Error in resource 'enterprise_organizations' processing enterprise '{ctx.enterprise_name}': {e}",
extra={"resource": "enterprise_organizations", "phase": "resource_iteration"},
)
return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/resources/enterprise.py` around lines 70 - 96, Add the
same graceful error handling used in enterprise() around the new
enterprise_organizations GraphQL pagination flow. Wrap the
ctx.client.paginate("/graphql", ...) loop in try/except (including HTTPError or
a broader request exception if consistent with the module), log the failure with
enough context, and return/stop cleanly instead of letting the exception escape.
Use the enterprise_organizations paginator setup and the ctx.client.paginate
call as the key locations to update.

Comment on lines +345 to +372
client = ctx.sso_client
if not client:
logger.info(
"Skipping enterprise_saml_provider for enterprise '%s': no SSO client configured",
ctx.enterprise_name,
)
return

data = {
"query": ENTERPRISE_SAML_QUERY,
"variables": {"slug": ctx.enterprise_name, "count": 1, "after": None},
"query": ENTERPRISE_SAML_PROVIDER_QUERY,
"variables": {"slug": ctx.enterprise_name},
}

for page_data in ctx.client.paginate(
"/graphql",
method="POST",
json=data,
paginator=paginator,
data_selector="data",
):
for enterprise_object in page_data:
es_data = enterprise_object.get("enterprise", {})
saml_provider = (es_data.get("ownerInfo") or {}).get("samlIdentityProvider")
if not saml_provider:
return
yield {
**{k: v for k, v in saml_provider.items() if k != "externalIdentities"},
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}
response = client.post("/graphql", json=data).json()
enterprise_object = (response.get("data") or {}).get("enterprise", {})
saml_provider = (enterprise_object.get("ownerInfo") or {}).get("samlIdentityProvider")
if not saml_provider:
logger.warning(
"No enterprise SAML provider returned for enterprise '%s'",
ctx.enterprise_name,
)
return

yield {
**saml_provider,
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing error handling around the SSO-backed GraphQL POST.

client.post("/graphql", json=data).json() at Line 358 is unwrapped, unlike enterprise()'s equivalent call in the same file/diff which catches exceptions and logs gracefully (Lines 53-62). A non-2xx response (e.g., insufficient PAT scope, transient network failure) raises HTTPError by default and will crash this resource instead of degrading gracefully like its sibling.

🛡️ Suggested fix
-    response = client.post("/graphql", json=data).json()
-    enterprise_object = (response.get("data") or {}).get("enterprise", {})
-    saml_provider = (enterprise_object.get("ownerInfo") or {}).get("samlIdentityProvider")
+    try:
+        response = client.post("/graphql", json=data).json()
+    except Exception as e:
+        logger.error(
+            f"Error in resource 'enterprise_saml_provider' processing enterprise '{ctx.enterprise_name}': {e}",
+            extra={"resource": "enterprise_saml_provider", "phase": "resource_iteration"},
+        )
+        return
+    enterprise_object = (response.get("data") or {}).get("enterprise", {})
+    saml_provider = (enterprise_object.get("ownerInfo") or {}).get("samlIdentityProvider")
📝 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
client = ctx.sso_client
if not client:
logger.info(
"Skipping enterprise_saml_provider for enterprise '%s': no SSO client configured",
ctx.enterprise_name,
)
return
data = {
"query": ENTERPRISE_SAML_QUERY,
"variables": {"slug": ctx.enterprise_name, "count": 1, "after": None},
"query": ENTERPRISE_SAML_PROVIDER_QUERY,
"variables": {"slug": ctx.enterprise_name},
}
for page_data in ctx.client.paginate(
"/graphql",
method="POST",
json=data,
paginator=paginator,
data_selector="data",
):
for enterprise_object in page_data:
es_data = enterprise_object.get("enterprise", {})
saml_provider = (es_data.get("ownerInfo") or {}).get("samlIdentityProvider")
if not saml_provider:
return
yield {
**{k: v for k, v in saml_provider.items() if k != "externalIdentities"},
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}
response = client.post("/graphql", json=data).json()
enterprise_object = (response.get("data") or {}).get("enterprise", {})
saml_provider = (enterprise_object.get("ownerInfo") or {}).get("samlIdentityProvider")
if not saml_provider:
logger.warning(
"No enterprise SAML provider returned for enterprise '%s'",
ctx.enterprise_name,
)
return
yield {
**saml_provider,
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}
client = ctx.sso_client
if not client:
logger.info(
"Skipping enterprise_saml_provider for enterprise '%s': no SSO client configured",
ctx.enterprise_name,
)
return
data = {
"query": ENTERPRISE_SAML_PROVIDER_QUERY,
"variables": {"slug": ctx.enterprise_name},
}
try:
response = client.post("/graphql", json=data).json()
except Exception as e:
logger.error(
f"Error in resource 'enterprise_saml_provider' processing enterprise '{ctx.enterprise_name}': {e}",
extra={"resource": "enterprise_saml_provider", "phase": "resource_iteration"},
)
return
enterprise_object = (response.get("data") or {}).get("enterprise", {})
saml_provider = (enterprise_object.get("ownerInfo") or {}).get("samlIdentityProvider")
if not saml_provider:
logger.warning(
"No enterprise SAML provider returned for enterprise '%s'",
ctx.enterprise_name,
)
return
yield {
**saml_provider,
"enterprise_node_id": enterprise_data.id,
"enterprise_slug": ctx.enterprise_name,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/resources/enterprise.py` around lines 345 - 372, Add
error handling around the SSO GraphQL request in enterprise_saml_provider: the
unguarded client.post("/graphql", json=data).json() call can raise on non-2xx or
network failures and should degrade gracefully like enterprise(). Wrap the
POST/JSON fetch in a try/except, log a warning or error with ctx.enterprise_name
and the exception details, then return early when the SSO-backed lookup fails;
keep the existing saml_provider parsing and yield logic unchanged when the
request succeeds.

- add normalized SAML models for:
  - `SAML_ServiceProvider`
  - `SAML_Issuer`
  - `SAML_AssertionConsumerService`
- wire normalized SAML resources into both organization and enterprise collection flows
- unify enterprise and organization SAML handling onto shared models instead of separate enterprise-only variants
- emit `SAML_Implements` edges from `GH_SamlIdentityProvider` to `SAML_ServiceProvider`
- emit `SAML_TrustsIssuer` and `SAML_HasAssertionConsumerService` from dedicated normalized models
- add `SAML_HasAccount` edges from the GitHub SAML service provider to mapped GitHub users when SAML account linkage exists
- enrich `GH_SamlIdentityProvider` output with provider details such as issuer, SSO URL, signing configuration, certificate data, and foreign environment context
- improve `GH_ExternalIdentity` mapping logic so foreign IdP matches can be constrained by provider-specific tenant/environment properties
- support both organization and enterprise scope metadata on normalized SAML nodes
- remove duplicated enterprise-specific SAML and external identity model implementations in favor of the shared path
@jaredcatkinson
jaredcatkinson force-pushed the fix/BED-8788-enterprise-sso-pat branch from c6970d1 to 2ef1c87 Compare July 14, 2026 05:20
- replace alert-number-only naming with repository-aware names
- set alert display names using the secret type when available
- make secret scanning alert nodes easier to distinguish in graph views and search results
- move extension metadata into the package so OpenHound can discover it correctly
- remove the repo-root `extension.yaml`
- update SAML provider lookup behavior to align with the current collected schema
- keep external identity resolution dependent on issuer and SSO URL derived context

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhound_github/resources/enterprise.py (1)

439-477: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add error handling around the enterprise_external_identity pagination call.

Like the previously-flagged enterprise_organizations and enterprise_saml_provider gaps, this new pagination call (client.paginate(...)) has no try/except. A transient/permission error on the SSO client will propagate unhandled and fail the whole resource, unlike the graceful-degradation pattern used elsewhere in this file and in organization.py's saml_provider/external_identities.

🛡️ Suggested fix
-    for page_data in client.paginate(
-        "/graphql",
-        method="POST",
-        json=data,
-        paginator=paginator,
-        data_selector="data",
-    ):
-        for enterprise_object in page_data:
+    try:
+        pages = client.paginate(
+            "/graphql",
+            method="POST",
+            json=data,
+            paginator=paginator,
+            data_selector="data",
+        )
+    except Exception as e:
+        logger.error(
+            f"Error in resource 'enterprise_external_identity' processing enterprise '{ctx.enterprise_name}': {e}",
+            extra={"resource": "enterprise_external_identity", "phase": "resource_iteration"},
+        )
+        return
+
+    for page_data in pages:
+        for enterprise_object in page_data:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/resources/enterprise.py` around lines 439 - 477, Wrap
the `client.paginate(...)` iteration in `enterprise_external_identity` with the
same targeted exception handling used by the existing
`enterprise_organizations`, `enterprise_saml_provider`, or `organization.py`
SAML flows. Log the pagination failure with enterprise context and return
gracefully so transient or permission errors do not fail the entire resource,
while preserving normal page processing.
🧹 Nitpick comments (7)
src/openhound_github/resources/enterprise.py (1)

478-489: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Remove the unused SAML-provider fields

ExternalIdentity only consumes environment_slug; the other keys are ignored here and never surface in the graph. Drop them unless you plan to add matching fields to the model/schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/resources/enterprise.py` around lines 478 - 489, Update
the external identity construction loop to yield only the SAML-provider field
consumed by ExternalIdentity: retain environment_slug and remove
environment_node_id, environment_name, saml_provider_id, saml_provider_issuer,
and saml_provider_sso_url.
src/openhound_github/models/__init__.py (1)

96-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate "ExternalIdentity" entry in __all__.

Harmless but confusing — appears at both line 99 and line 101.

🧹 Proposed fix
     "SamlAssertionConsumerService",
     "SamlProvider",
     "SamlServiceProvider",
-    "ExternalIdentity",
     "SamlIssuer",
     "ExternalIdentity",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/__init__.py` around lines 96 - 101, Remove the
duplicate "ExternalIdentity" entry from the __all__ list in
src/openhound_github/models/__init__.py, keeping a single export entry alongside
the other model symbols.
src/openhound_github/models/external_identity.py (2)

202-202: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

id shadows the Python builtin.

Minor style nit flagged by Ruff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/external_identity.py` at line 202, Rename the
local variable id in the external identity unpacking assignment to a
non-shadowing name, and update its uses within the surrounding function
accordingly.

Source: Linters/SAST tools


293-301: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Thread environment_type through here instead of inferring it from environment_node_id. idp_for_environment(...) already has the SAML provider row; include the explicit environment_type column and reuse it so service_provider_node_id stays aligned with the normalized SAML nodes and SAML_HAS_ACCOUNT doesn’t point at the wrong target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/external_identity.py` around lines 293 - 301,
Update ExternalIdentity.service_provider_node_id to obtain and reuse the
explicit environment_type from idp_for_environment(...)’s SAML provider row,
rather than inferring it from environment_node_id. Thread the environment_type
column through the provider lookup and preserve the existing None behavior when
required environment data is unavailable, ensuring the generated node ID matches
normalized SAML nodes.
src/openhound_github/models/saml_provider.py (1)

89-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate foreign-IdP detection logic, and its result is discarded.

detect_foreign_environment duplicates ExternalIdentity.detect_foreign_idp (in external_identity.py) with divergent naming for the same IdPs ("PingOneUser"/"AZUser"/"Okta_User" here vs "pingone"/"entra"/"okta" there). In as_node (line 105) the returned foreign_idp_type is discarded (_), so this duplication currently has no functional effect, but the divergence is a maintenance hazard if either side is later relied upon. Consider extracting a single shared helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/saml_provider.py` around lines 89 - 101, Remove
the duplicate IdP detection logic from SAMLProvider.detect_foreign_environment
and reuse the shared ExternalIdentity.detect_foreign_idp helper instead. Update
as_node to preserve and use the helper’s canonical IdP values rather than
discarding the returned foreign_idp_type, or remove the unused wrapper if no
caller requires it.
src/openhound_github/models/saml_service_provider.py (1)

90-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused unpacked variable acs_url.

Ruff flags acs_url as unused; only sp_entity_id is consumed.

🧹 Proposed fix
-        acs_url, sp_entity_id = self.saml_route
+        _acs_url, sp_entity_id = self.saml_route
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/saml_service_provider.py` around lines 90 - 93,
Update the as_node property’s saml_route unpacking to avoid binding the unused
acs_url value while preserving the existing use of sp_entity_id and subsequent
scope_type logic.

Source: Linters/SAST tools

src/openhound_github/kinds/edges.py (1)

143-155: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove duplicate SAML_TRUSTS_ISSUER assignment.

Line 148 duplicates line 147.

🧹 Proposed fix
 SAML_TRUSTS_ISSUER = "SAML_TrustsIssuer"
-SAML_TRUSTS_ISSUER = "SAML_TrustsIssuer"
 SAML_ISSUES_ASSERTIONS_TO = "SAML_IssuesAssertionsTo"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/kinds/edges.py` around lines 143 - 155, Remove the
duplicate SAML_TRUSTS_ISSUER assignment in the SAML edges constants, retaining a
single definition with the existing value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhound_github/models/external_identity.py`:
- Around line 211-292: Update the match_key selection in the SYNCED_TO_GH_USER
block of _maps_to_user_edges to use "login" for Okta, matching the MAPS_TO_USER
block, while preserving "email" for PingOne and "name" as the default. Prefer
reusing a shared match-key computation if practical so both edge blocks cannot
diverge.
- Around line 273-278: Escape the IdP-supplied foreign_username before
interpolating it into the Cypher query assigned to q, while preserving the
existing uppercase comparison and query behavior. Apply the escaping at the
value used by the WHERE n.name condition so embedded single quotes cannot alter
the composed query.

---

Outside diff comments:
In `@src/openhound_github/resources/enterprise.py`:
- Around line 439-477: Wrap the `client.paginate(...)` iteration in
`enterprise_external_identity` with the same targeted exception handling used by
the existing `enterprise_organizations`, `enterprise_saml_provider`, or
`organization.py` SAML flows. Log the pagination failure with enterprise context
and return gracefully so transient or permission errors do not fail the entire
resource, while preserving normal page processing.

---

Nitpick comments:
In `@src/openhound_github/kinds/edges.py`:
- Around line 143-155: Remove the duplicate SAML_TRUSTS_ISSUER assignment in the
SAML edges constants, retaining a single definition with the existing value.

In `@src/openhound_github/models/__init__.py`:
- Around line 96-101: Remove the duplicate "ExternalIdentity" entry from the
__all__ list in src/openhound_github/models/__init__.py, keeping a single export
entry alongside the other model symbols.

In `@src/openhound_github/models/external_identity.py`:
- Line 202: Rename the local variable id in the external identity unpacking
assignment to a non-shadowing name, and update its uses within the surrounding
function accordingly.
- Around line 293-301: Update ExternalIdentity.service_provider_node_id to
obtain and reuse the explicit environment_type from idp_for_environment(...)’s
SAML provider row, rather than inferring it from environment_node_id. Thread the
environment_type column through the provider lookup and preserve the existing
None behavior when required environment data is unavailable, ensuring the
generated node ID matches normalized SAML nodes.

In `@src/openhound_github/models/saml_provider.py`:
- Around line 89-101: Remove the duplicate IdP detection logic from
SAMLProvider.detect_foreign_environment and reuse the shared
ExternalIdentity.detect_foreign_idp helper instead. Update as_node to preserve
and use the helper’s canonical IdP values rather than discarding the returned
foreign_idp_type, or remove the unused wrapper if no caller requires it.

In `@src/openhound_github/models/saml_service_provider.py`:
- Around line 90-93: Update the as_node property’s saml_route unpacking to avoid
binding the unused acs_url value while preserving the existing use of
sp_entity_id and subsequent scope_type logic.

In `@src/openhound_github/resources/enterprise.py`:
- Around line 478-489: Update the external identity construction loop to yield
only the SAML-provider field consumed by ExternalIdentity: retain
environment_slug and remove environment_node_id, environment_name,
saml_provider_id, saml_provider_issuer, and saml_provider_sso_url.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25442157-24b2-4c32-833e-a43fb94ded43

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd0bca and c6970d1.

📒 Files selected for processing (12)
  • src/openhound_github/kinds/edges.py
  • src/openhound_github/kinds/nodes.py
  • src/openhound_github/models/__init__.py
  • src/openhound_github/models/enterprise_external_identity.py
  • src/openhound_github/models/enterprise_saml_provider.py
  • src/openhound_github/models/external_identity.py
  • src/openhound_github/models/saml_assertion_consumer_service.py
  • src/openhound_github/models/saml_issuer.py
  • src/openhound_github/models/saml_provider.py
  • src/openhound_github/models/saml_service_provider.py
  • src/openhound_github/resources/enterprise.py
  • src/openhound_github/resources/organization.py
💤 Files with no reviewable changes (2)
  • src/openhound_github/models/enterprise_saml_provider.py
  • src/openhound_github/models/enterprise_external_identity.py

Comment thread src/openhound_github/models/external_identity.py
Comment on lines +273 to +278
q = (
f"MATCH p=()<-[:GH_SyncedToEnvironment]-(:GH_SamlIdentityProvider)"
f"-[:GH_HasExternalIdentity]->(:GH_ExternalIdentity)"
f"-[:GH_MapsToUser]->(n) "
f"WHERE n.objectid = '{gh_id}' OR n.name = '{foreign_username.upper()}' RETURN p"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether other query_composition/query_* string builders in this codebase escape single quotes.
rg -n "query_composition|query_mapped_users|query_environments" --type=py -C2

Repository: SpecterOps/openhound-github

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file structure first, then inspect the relevant section.
ast-grep outline src/openhound_github/models/external_identity.py --view expanded || true

echo '--- lines around the reported query ---'
sed -n '240,320p' src/openhound_github/models/external_identity.py

echo '--- search for similar Cypher query construction in this file and related models ---'
rg -n "MATCH p=|RETURN p|foreign_username|gh_id|Cypher|cypher|session\.run|query_" src/openhound_github/models -C 2 || true

Repository: SpecterOps/openhound-github

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- find query_composition definition and usages ---'
rg -n "query_composition" src -C 2

echo '--- inspect the edge/property types around query_composition ---'
sed -n '1,240p' src/openhound_github/models/edge.py

echo '--- inspect the external identity model around the composed query ---'
sed -n '210,300p' src/openhound_github/models/external_identity.py

echo '--- inspect any code that executes composed queries ---'
rg -n "query_composition|composed=True|RETURN p" src/openhound_github -C 2 | sed -n '1,240p'

Repository: SpecterOps/openhound-github

Length of output: 2824


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- graph.py definition of edge properties ---'
sed -n '1,120p' src/openhound_github/graph.py

echo '--- locate all query_composition field reads and composed-edge handling ---'
rg -n "\.query_composition|query_composition\s*=|composed\s*=\s*True|composed" src/openhound_github -C 3

echo '--- inspect the query builder helpers used in other composed edges for escaping/parameterization patterns ---'
sed -n '1,260p' src/openhound_github/models/org_secret.py
sed -n '200,310p' src/openhound_github/models/secret_scanning_alert.py

Repository: SpecterOps/openhound-github

Length of output: 16302


Escape foreign_username before building this composed Cypher query. foreign_username.upper() is interpolated directly into WHERE n.name = '...', so an IdP-supplied value containing ' can alter the query. src/openhound_github/models/external_identity.py:273-277

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/external_identity.py` around lines 273 - 278,
Escape the IdP-supplied foreign_username before interpolating it into the Cypher
query assigned to q, while preserving the existing uppercase comparison and
query behavior. Apply the escaping at the value used by the WHERE n.name
condition so embedded single quotes cannot alter the composed query.

… mapping

- move shared SAML parsing and node-id construction into saml_helpers
- reuse shared helpers across SAML provider, issuer, ACS, and external identity models
- stop inferring service provider environment type from node id prefixes
- remove duplicated foreign IdP matching logic in external identity edges
- escape composed Cypher username matching for synced identity queries

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/openhound_github/resources/enterprise.py (1)

350-372: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing error handling around PAT-backed SSO GraphQL calls.

client.post(...) (Line 364) and client.paginate(...) (Lines 462-468) — both using ctx.sso_client — are unwrapped, unlike enterprise() in this same file, which catches exceptions and logs gracefully. A non-2xx response (insufficient PAT scope, transient failure) will raise and crash the whole enterprise SAML/identity pipeline instead of degrading gracefully. The client.post case at Line 364 was already flagged in a prior review round and remains unresolved in this version; the client.paginate call at Lines 462-468 has the identical hazard.

🛡️ Suggested fix
-    response = client.post("/graphql", json=data).json()
+    try:
+        response = client.post("/graphql", json=data).json()
+    except Exception as e:
+        logger.error(
+            f"Error in resource 'enterprise_saml_provider' processing enterprise '{ctx.enterprise_name}': {e}",
+            extra={"resource": "enterprise_saml_provider", "phase": "resource_iteration"},
+        )
+        return
     enterprise_object = (response.get("data") or {}).get("enterprise", {})
-    for page_data in client.paginate(
-        "/graphql",
-        method="POST",
-        json=data,
-        paginator=paginator,
-        data_selector="data",
-    ):
+    try:
+        pages = list(client.paginate(
+            "/graphql",
+            method="POST",
+            json=data,
+            paginator=paginator,
+            data_selector="data",
+        ))
+    except Exception as e:
+        logger.error(
+            f"Error in resource 'enterprise_external_identity' processing enterprise '{ctx.enterprise_name}': {e}",
+            extra={"resource": "enterprise_external_identity", "phase": "resource_iteration"},
+        )
+        return
+    for page_data in pages:

Also applies to: 462-468

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/resources/enterprise.py` around lines 350 - 372, Wrap
the PAT-backed SSO requests in enterprise_saml_provider and the corresponding
identity-provider pagination flow around client.post and client.paginate with
the same exception handling pattern used by enterprise(). Log the failure with
enterprise context and return safely so missing scopes or transient GraphQL
errors skip these resources without crashing the pipeline.
🧹 Nitpick comments (2)
src/openhound_github/resources/organization.py (1)

1736-1738: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the nonexistent argument from the docstring.

The docstring documents a saml (SamlProvider) argument, but the function signature only accepts ctx.

♻️ Proposed fix
     Args:
-        saml (SamlProvider): The SAML provider to extract identities for.
         ctx (SourceContext): The shared context containing the REST client and organization name.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/resources/organization.py` around lines 1736 - 1738,
Remove the `saml (SamlProvider)` entry from the function docstring’s Args
section, leaving only documentation for the actual `ctx` parameter.
src/openhound_github/models/saml_helpers.py (1)

20-22: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use urllib.parse to safely extract the domain.

Extracting the domain via .split("/")[2] will throw an IndexError if the sso_url is malformed and lacks a protocol scheme like https://. Using urllib.parse.urlparse is safer and more idiomatic.

♻️ Proposed refactor

Add the import at the top of the file:

+from urllib.parse import urlparse

And update the Okta domain extraction logic:

     if issuer.startswith("http://www.okta.com/"):
-        domain = (sso_url or "").split("/")[2] if sso_url else None
+        domain = urlparse(sso_url).netloc if sso_url else None
         return "okta", domain
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/saml_helpers.py` around lines 20 - 22, Update the
Okta handling in the issuer-detection function to use urllib.parse.urlparse for
extracting the SSO hostname instead of indexing sso_url.split("/"). Preserve the
existing None behavior when sso_url is absent and safely handle malformed URLs
without raising IndexError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/openhound_github/resources/enterprise.py`:
- Around line 350-372: Wrap the PAT-backed SSO requests in
enterprise_saml_provider and the corresponding identity-provider pagination flow
around client.post and client.paginate with the same exception handling pattern
used by enterprise(). Log the failure with enterprise context and return safely
so missing scopes or transient GraphQL errors skip these resources without
crashing the pipeline.

---

Nitpick comments:
In `@src/openhound_github/models/saml_helpers.py`:
- Around line 20-22: Update the Okta handling in the issuer-detection function
to use urllib.parse.urlparse for extracting the SSO hostname instead of indexing
sso_url.split("/"). Preserve the existing None behavior when sso_url is absent
and safely handle malformed URLs without raising IndexError.

In `@src/openhound_github/resources/organization.py`:
- Around line 1736-1738: Remove the `saml (SamlProvider)` entry from the
function docstring’s Args section, leaving only documentation for the actual
`ctx` parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c192d69-44cd-4a99-bb72-3b887c5b11c9

📥 Commits

Reviewing files that changed from the base of the PR and between c6970d1 and bc4537e.

📒 Files selected for processing (22)
  • src/openhound_github/extension.yaml
  • src/openhound_github/graphql.py
  • src/openhound_github/kinds/edges.py
  • src/openhound_github/kinds/nodes.py
  • src/openhound_github/lookup.py
  • src/openhound_github/models/__init__.py
  • src/openhound_github/models/enterprise_external_identity.py
  • src/openhound_github/models/enterprise_saml_provider.py
  • src/openhound_github/models/external_identity.py
  • src/openhound_github/models/repository.py
  • src/openhound_github/models/saml_assertion_consumer_service.py
  • src/openhound_github/models/saml_helpers.py
  • src/openhound_github/models/saml_issuer.py
  • src/openhound_github/models/saml_provider.py
  • src/openhound_github/models/saml_service_provider.py
  • src/openhound_github/models/secret_scanning_alert.py
  • src/openhound_github/models/workflow.py
  • src/openhound_github/models/workflow_job.py
  • src/openhound_github/models/workflow_step.py
  • src/openhound_github/resources/enterprise.py
  • src/openhound_github/resources/organization.py
  • src/openhound_github/transforms.py
💤 Files with no reviewable changes (2)
  • src/openhound_github/models/enterprise_external_identity.py
  • src/openhound_github/models/enterprise_saml_provider.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/openhound_github/kinds/nodes.py
  • src/openhound_github/models/saml_issuer.py
  • src/openhound_github/models/init.py
  • src/openhound_github/models/saml_assertion_consumer_service.py
  • src/openhound_github/models/saml_service_provider.py
  • src/openhound_github/kinds/edges.py
  • src/openhound_github/models/external_identity.py

- use urllib.parse.urlparse to safely extract Okta SSO hostnames
- remove the stale saml argument entry from external_identities docstring
- add GH_EnvironmentBranchPolicy nodes and GH_MatchesEnvironmentPolicy edges
- model GH_CanDeployToEnvironment for unrestricted, protected, and custom-policy environments
- add GH_CanEditEnvironment and GH_ApprovesDeploymentTo relationships
- surface environment protection metadata including reviewers, wait timers, and admin bypass
- switch branch and environment containment to GH_Contains for composition queries
- add protected-branch fallback handling when no branch protection rules exist
- add tests for branch policy matching and protected-branch fallback behavior
- add GH_HasSecret and GH_HasVariable edges from environments to scoped values
- resolve GH_EnvironmentSecret references from workflow steps with literal job environments
- resolve GH_EnvironmentVariable references from workflow steps with literal job environments
- add repository access edges for organization variables across all, private, and selected visibility scopes
- preserve repository and organization secret/variable resolution alongside environment-scoped matches
- add GH_CanCreateEnvironment edges from repo roles that can create branches
- model environment creation through workflow edits that reference new environment names
- require write-equivalent repository access before emitting the edge
- add schema entries for environment branch policy and environment deployment edges
- update saved searches to use GH_Contains instead of legacy GH_Has* relationships
- document GH_EnvironmentBranchPolicy and environment deployment relationships
- document environment-scoped secret and variable resolution
- refresh containment and workflow reference docs for environment support

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/openhound_github/models/environment_branch_policy.py (3)

43-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring is stale — this asset now emits a node.

📝 Proposed docstring update
-    """One record from `environment_branch_policies` → GH_Contains edge from synthetic policy ID. No node."""
+    """One record from `environment_branch_policies` → one GH_EnvironmentBranchPolicy node
+    + GH_Contains edge from the environment + branch match/deploy edges."""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/environment_branch_policy.py` around lines 43 -
44, Update the EnvironmentBranchPolicy class docstring to remove the claim that
it emits no node and accurately describe its current node-emission behavior,
while preserving the existing record and GH_Contains relationship context.

129-152: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Hoist environment_protected_branches out of the branch loop.

It is a property that issues a DuckDB query on every access, and it is read up to twice per matching branch. Evaluate it once per asset.

⚡ Proposed fix
         branches = self._lookup.branches_for_repository(self.repository_node_id)
+        protected_only = self.environment_protected_branches
         for branch_id, branch_name, protected in branches:
             if self.matches_branch(branch_name):
                 yield Edge(
                     kind=ek.MATCHES_ENVIRONMENT_POLICY,
                     start=EdgePath(value=branch_id, match_by="id"),
                     end=EdgePath(value=self.node_id, match_by="id"),
                     properties=EdgeProperties(traversable=False),
                 )
-                if not self.environment_protected_branches or protected:
+                if not protected_only or protected:
                     yield Edge(
                         kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
                         start=EdgePath(value=branch_id, match_by="id"),
                         end=EdgePath(value=self.environment_node_id, match_by="id"),
                         properties=GHEdgeProperties(
                             traversable=True,
                             composed=True,
                             query_composition=(
                                 self._protected_policy_branch_can_deploy_query(branch_id)
-                                if self.environment_protected_branches
+                                if protected_only
                                 else self._policy_branch_can_deploy_query(branch_id)
                             ),
                         ),
                     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/environment_branch_policy.py` around lines 129 -
152, Evaluate self.environment_protected_branches once before iterating over
branches in the branch-processing method, store the result in a local variable,
and use that variable for both the deployment condition and query selection
inside the loop. Keep the existing behavior unchanged while avoiding repeated
property access per matching branch.

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead policy_id helper and commented _b64 import. as_node and edges already use self.node_id, so the property is unused and can be dropped along with the commented import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/environment_branch_policy.py` at line 12, Remove
the unused commented `_b64` import and delete the dead `policy_id` property from
the environment branch policy model. Preserve the existing `as_node` and `edges`
implementations using `self.node_id`.
src/openhound_github/models/org_variable.py (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for the new environment/org secret & variable reachability edges.

These four sites introduce new, security-relevant graph edges (traversable env/org→secret/variable access paths and environment-scoped USES_SECRET/USES_VARIABLE edges), but none has direct unit test coverage, unlike the sibling Environment/EnvironmentBranchPolicy edge tests added in tests/test_environment_model.py.

  • src/openhound_github/models/org_variable.py#L101-140: add tests for OrgVariable.edges covering visibility="all", "private", and default/unhandled cases, verifying the CONTAINS edge and the repo→variable HAS_VARIABLE edges.
  • src/openhound_github/models/env_secret.py#L50-57,112-117: add a test asserting EnvironmentSecret.edges yields both the non-traversable CONTAINS edge and the traversable HAS_SECRET edge.
  • src/openhound_github/models/env_variable.py#L54-61,116-121: add a test asserting EnvironmentVariable.edges yields both the non-traversable CONTAINS edge and the traversable HAS_VARIABLE edge.
  • src/openhound_github/models/workflow_step.py#L230-252,287-313: add a test for _uses_secret_edges/_uses_variable_edges covering the job_environment branch (including the "${{" in job_environment guard skipping the lookup).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/org_variable.py` at line 1, Add unit tests for
the new graph edges in OrgVariable.edges, EnvironmentSecret.edges,
EnvironmentVariable.edges, and
WorkflowStep._uses_secret_edges/_uses_variable_edges. Cover OrgVariable
visibility values "all", "private", and default/unhandled behavior, including
CONTAINS and repository HAS_VARIABLE edges; assert environment secret/variable
edges include both non-traversable CONTAINS and traversable
HAS_SECRET/HAS_VARIABLE edges; and cover the workflow step job_environment path,
including skipping lookup when the value contains "${{".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhound_github/models/environment.py`:
- Around line 141-154: Remove the two CAN_DEPLOY_TO_ENVIRONMENT EdgeDefs in the
environment model that use nk.USER and nk.TEAM as start nodes. Keep the
repository, branch, and {repo}_admin deploy edges and the existing
APPROVES_DEPLOYMENT_TO definitions unchanged.

---

Nitpick comments:
In `@src/openhound_github/models/environment_branch_policy.py`:
- Around line 43-44: Update the EnvironmentBranchPolicy class docstring to
remove the claim that it emits no node and accurately describe its current
node-emission behavior, while preserving the existing record and GH_Contains
relationship context.
- Around line 129-152: Evaluate self.environment_protected_branches once before
iterating over branches in the branch-processing method, store the result in a
local variable, and use that variable for both the deployment condition and
query selection inside the loop. Keep the existing behavior unchanged while
avoiding repeated property access per matching branch.
- Line 12: Remove the unused commented `_b64` import and delete the dead
`policy_id` property from the environment branch policy model. Preserve the
existing `as_node` and `edges` implementations using `self.node_id`.

In `@src/openhound_github/models/org_variable.py`:
- Line 1: Add unit tests for the new graph edges in OrgVariable.edges,
EnvironmentSecret.edges, EnvironmentVariable.edges, and
WorkflowStep._uses_secret_edges/_uses_variable_edges. Cover OrgVariable
visibility values "all", "private", and default/unhandled behavior, including
CONTAINS and repository HAS_VARIABLE edges; assert environment secret/variable
edges include both non-traversable CONTAINS and traversable
HAS_SECRET/HAS_VARIABLE edges; and cover the workflow step job_environment path,
including skipping lookup when the value contains "${{".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ecd4ee3-189c-4553-94ef-883e245aafbd

📥 Commits

Reviewing files that changed from the base of the PR and between bc4537e and 1051f07.

📒 Files selected for processing (40)
  • descriptions/edges/GH_ApprovesDeploymentTo.md
  • descriptions/edges/GH_CanCreateEnvironment.md
  • descriptions/edges/GH_CanDeployToEnvironment.md
  • descriptions/edges/GH_CanEditEnvironment.md
  • descriptions/edges/GH_Contains.md
  • descriptions/edges/GH_DeploysTo.md
  • descriptions/edges/GH_HasBranch.md
  • descriptions/edges/GH_HasEnvironment.md
  • descriptions/edges/GH_HasSecret.md
  • descriptions/edges/GH_HasVariable.md
  • descriptions/edges/GH_MatchesEnvironmentPolicy.md
  • descriptions/edges/GH_UsesSecret.md
  • descriptions/edges/GH_UsesVariable.md
  • descriptions/nodes/GH_Environment.md
  • descriptions/nodes/GH_EnvironmentBranchPolicy.md
  • descriptions/nodes/GH_EnvironmentSecret.md
  • descriptions/nodes/GH_EnvironmentVariable.md
  • extension/saved_searches/environments-admin-bypass.json
  • extension/saved_searches/repository-workflows.json
  • extension/saved_searches/unprotected-branches.json
  • extension/saved_searches/unprotected-default-branch-with-workflow.json
  • extension/saved_searches/unprotected-default-branches.json
  • extension/schema.json
  • src/openhound_github/kinds/edges.py
  • src/openhound_github/kinds/nodes.py
  • src/openhound_github/lookup.py
  • src/openhound_github/models/branch.py
  • src/openhound_github/models/env_secret.py
  • src/openhound_github/models/env_variable.py
  • src/openhound_github/models/environment.py
  • src/openhound_github/models/environment_branch_policy.py
  • src/openhound_github/models/org_variable.py
  • src/openhound_github/models/repository.py
  • src/openhound_github/models/repository_role.py
  • src/openhound_github/models/saml_helpers.py
  • src/openhound_github/models/workflow.py
  • src/openhound_github/models/workflow_step.py
  • src/openhound_github/resources/organization.py
  • tests/test_environment_model.py
  • tests/test_workflow_model.py
💤 Files with no reviewable changes (1)
  • src/openhound_github/resources/organization.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/openhound_github/models/saml_helpers.py
  • src/openhound_github/models/workflow.py

Comment on lines +141 to +154
EdgeDef(
start=nk.USER,
end=nk.ENVIRONMENT,
kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
description="Reviewer user can approve deployment to environment",
traversable=True,
),
EdgeDef(
start=nk.TEAM,
end=nk.ENVIRONMENT,
kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
description="Reviewer team can approve deployment to environment",
traversable=True,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

These two CAN_DEPLOY_TO_ENVIRONMENT EdgeDefs are never emitted and contradict the documented model.

edges() only yields CAN_DEPLOY_TO_ENVIRONMENT from the repository, branches, and the {repo}_admin role — never from GH_User/GH_Team. descriptions/edges/GH_CanDeployToEnvironment.md also states reviewers are represented via GH_ApprovesDeploymentTo metadata rather than extra deploy edges. Reviewer coverage is already declared by the two APPROVES_DEPLOYMENT_TO defs below.

🧹 Proposed removal of the unemitted reviewer deploy EdgeDefs
-        EdgeDef(
-            start=nk.USER,
-            end=nk.ENVIRONMENT,
-            kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
-            description="Reviewer user can approve deployment to environment",
-            traversable=True,
-        ),
-        EdgeDef(
-            start=nk.TEAM,
-            end=nk.ENVIRONMENT,
-            kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
-            description="Reviewer team can approve deployment to environment",
-            traversable=True,
-        ),
         EdgeDef(
             start=nk.USER,
             end=nk.ENVIRONMENT,
             kind=ek.APPROVES_DEPLOYMENT_TO,
📝 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
EdgeDef(
start=nk.USER,
end=nk.ENVIRONMENT,
kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
description="Reviewer user can approve deployment to environment",
traversable=True,
),
EdgeDef(
start=nk.TEAM,
end=nk.ENVIRONMENT,
kind=ek.CAN_DEPLOY_TO_ENVIRONMENT,
description="Reviewer team can approve deployment to environment",
traversable=True,
),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/environment.py` around lines 141 - 154, Remove
the two CAN_DEPLOY_TO_ENVIRONMENT EdgeDefs in the environment model that use
nk.USER and nk.TEAM as start nodes. Keep the repository, branch, and
{repo}_admin deploy edges and the existing APPROVES_DEPLOYMENT_TO definitions
unchanged.

…ity matching

- add deployment-aware normalized SAML IDs and routes for GitHub.com and GHES
- emit normalized SAML service provider, issuer, and ACS nodes with contract metadata
- anchor SAML_Implements edges from GH_SamlIdentityProvider nodes
- add schema contract metadata to normalized SAML relationship edges
- enrich SAML_HasAccount edges with direct binding evidence and match values
- preserve legacy GH_SamlIdentityProvider and GH_ExternalIdentity relationships
- add tenant-scoped foreign identity matching for Entra, Okta, and PingOne
- require Entra tenant/object claims to agree with the SAML issuer before emitting correlations
- expose GitHub deployment metadata on legacy SAML provider nodes
- harden SAML provider replay for DLT snake_case rows and GraphQL aliases
- add regression coverage for deployment scoping, normalized SAML output, and tenant-safe identity resolution

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/openhound_github/models/external_identity.py (1)

242-269: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Composed SYNCED_TO_GH_USER Cypher query is still injectable and drops tenant scoping.

Two problems in the same block:

  1. matcher.value (an uppercased SAML/SCIM username, or an Entra objectid claim — IdP-supplied data) is interpolated unescaped into a single-quoted Cypher literal. A value containing ' breaks or alters the query. This mirrors a previously flagged (and apparently unresolved) review comment on this same code path.
  2. composition_matchers filters to only {"name", "objectid"}, dropping the tenant-scoping key (tenant_domain/environmentid/tenantid) that matchers otherwise carries. Since the MATCH pattern isn't anchored to a specific IdP node, two different tenants with a duplicate normalized username (exactly the scenario in test_external_identity_scopes_duplicate_okta_username_by_tenant) can produce an ambiguous/incorrect composed path — even though the ConditionalEdgePath endpoints are correctly tenant-scoped. This gap isn't caught by existing tests, which only assert on property_matchers, never on query_composition content.
🐛 Proposed fix
         if matchers and self.user and self.user.id:
-            composition_matchers = [
-                matcher for matcher in matchers if matcher.key in {"name", "objectid"}
-            ]
-            composition_predicates = " OR ".join(
-                f"n.{matcher.key} = '{matcher.value}'"
-                for matcher in composition_matchers
-            )
+            def _escape(value: str) -> str:
+                return value.replace("\\", "\\\\").replace("'", "\\'")
+
+            composition_predicates = " AND ".join(
+                f"n.{matcher.key} = '{_escape(matcher.value)}'"
+                for matcher in matchers
+            )
             q = (
                 f"MATCH p=()<-[:GH_SyncedToEnvironment]-(:GH_SamlIdentityProvider)"
                 f"-[:GH_HasExternalIdentity]->(:GH_ExternalIdentity)"
                 f"-[:GH_MapsToUser]->(n) "
                 f"WHERE {composition_predicates} RETURN p"
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/external_identity.py` around lines 242 - 269, Fix
the composed query construction in the SYNCED_TO_GH_USER block: parameterize or
safely escape matcher.value before using it in Cypher literals, and retain the
tenant-scoping matcher (tenant_domain, environmentid, or tenantid) when building
composition_matchers. Ensure query_composition constrains both the identity
value and its tenant while preserving the existing composed Edge behavior.
🧹 Nitpick comments (1)
src/openhound_github/models/saml_service_provider.py (1)

67-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared SAML-asset environment/deployment scoping into a common base/mixin.

github_deployment_id/github_web_origin defaults and the service_provider_node_id property (delegating to github_saml_service_provider_id) are copy/pasted verbatim across three asset classes. This is the same duplication pattern that previously caused the Okta match_key divergence bug between two near-identical edge blocks — worth deduplicating before a fourth normalized SAML asset is added.

  • src/openhound_github/models/saml_service_provider.py#L67-L83: extract github_deployment_id/github_web_origin fields and service_provider_node_id into a shared base class/mixin that SamlServiceProvider inherits from.
  • src/openhound_github/models/saml_issuer.py#L60-L85: inherit the same shared base/mixin instead of redeclaring the fields and service_provider_node_id property.
  • src/openhound_github/models/saml_assertion_consumer_service.py#L63-L77: inherit the same shared base/mixin instead of redeclaring the fields and service_provider_node_id property.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhound_github/models/saml_service_provider.py` around lines 67 - 83,
Extract the shared github_deployment_id, github_web_origin, and
service_provider_node_id implementation into a common SAML asset base class or
mixin, preserving delegation to github_saml_service_provider_id. Update
src/openhound_github/models/saml_service_provider.py:67-83 to inherit it and
remove the duplicate members; update
src/openhound_github/models/saml_issuer.py:60-85 and
src/openhound_github/models/saml_assertion_consumer_service.py:63-77 likewise,
leaving each class’s asset-specific fields intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/openhound_github/models/external_identity.py`:
- Around line 242-269: Fix the composed query construction in the
SYNCED_TO_GH_USER block: parameterize or safely escape matcher.value before
using it in Cypher literals, and retain the tenant-scoping matcher
(tenant_domain, environmentid, or tenantid) when building composition_matchers.
Ensure query_composition constrains both the identity value and its tenant while
preserving the existing composed Edge behavior.

---

Nitpick comments:
In `@src/openhound_github/models/saml_service_provider.py`:
- Around line 67-83: Extract the shared github_deployment_id, github_web_origin,
and service_provider_node_id implementation into a common SAML asset base class
or mixin, preserving delegation to github_saml_service_provider_id. Update
src/openhound_github/models/saml_service_provider.py:67-83 to inherit it and
remove the duplicate members; update
src/openhound_github/models/saml_issuer.py:60-85 and
src/openhound_github/models/saml_assertion_consumer_service.py:63-77 likewise,
leaving each class’s asset-specific fields intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60abc960-ef2c-4d86-832a-8da6202e5b8d

📥 Commits

Reviewing files that changed from the base of the PR and between 1051f07 and ab173f2.

📒 Files selected for processing (14)
  • src/openhound_github/graphql.py
  • src/openhound_github/kinds/nodes.py
  • src/openhound_github/models/external_identity.py
  • src/openhound_github/models/saml_assertion_consumer_service.py
  • src/openhound_github/models/saml_helpers.py
  • src/openhound_github/models/saml_issuer.py
  • src/openhound_github/models/saml_provider.py
  • src/openhound_github/models/saml_service_provider.py
  • src/openhound_github/resources/enterprise.py
  • src/openhound_github/resources/organization.py
  • src/openhound_github/source.py
  • tests/test_external_identity_tenant_matching.py
  • tests/test_saml_helpers.py
  • tests/test_saml_models.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/openhound_github/kinds/nodes.py
  • src/openhound_github/models/saml_provider.py
  • src/openhound_github/graphql.py
  • src/openhound_github/resources/organization.py
  • src/openhound_github/resources/enterprise.py

- emit normalized SCIM organization, user, and group nodes
- add SCIM contains, membership, and provisioning relationships
- collect enterprise SCIM users and groups from GitHub SCIM endpoints
- add optional legacy Okta-to-SCIM correlation support
- connect SCIM groups to enterprise teams when group IDs are present
- add SCIM lookup helpers, source configuration, and regression coverage
- document enterprise SCIM collection and hybrid correlation behavior
map enterprise role permissions into granular GH_* capability edges
keep only confirmed enterprise privilege paths traversable
add synthetic members enterprise role for consistent role modeling
assign every enterprise user to the built-in members role
avoid assignment API lookups for synthetic owners and members roles
register new enterprise capability relationships in the schema
add tests covering permission mapping, traversability, and synthetic role behavior
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