Skip to content

feat: Delete Extension Endpoint - BED-6898#2339

Merged
kpowderly merged 51 commits intomainfrom
BED-6898
Feb 10, 2026
Merged

feat: Delete Extension Endpoint - BED-6898#2339
kpowderly merged 51 commits intomainfrom
BED-6898

Conversation

@kpowderly
Copy link
Copy Markdown
Contributor

@kpowderly kpowderly commented Feb 3, 2026

Description

Delete Extension Endpoint

Removal deletes the extension and removes all schema components tied to that extension:

  • graph schema (i.e. nodes and relationships/edges)
  • environments
  • findings & remediation

Since we have cascade deletes, this functionality works and it has been validated in the cascade delete integration test

Instead of abstracting that we cannot delete builtin extensions, this was moved back into the database method: https://github.com/SpecterOps/BloodHound/pull/2339/changes#diff-afc4fdb9518a128b497afb174e52ee860b5f75dd2b82d84db6ef76b3e1da701cR215

Motivation and Context

  • Returns 401 if no user is found
  • Returns 403 if user doesn't have proper permissions (Administrator Role)
  • Returns 400 if the id passed in is malformed
  • Returns 404 if database returns not found
  • Returns 204 on Success

Resolves BED-6898

How Has This Been Tested?

Unit & integration tests

Manual Testing Steps performed:

  1. Ran BHE Dev
  2. Flipped Open Graph Feature Flags to true
  3. Inserted an extension with REST endpoint
  4. Validated the extension and it's dependents were created in database

curl --request PUT \ --url http://bhe.localhost/api/v2/extensions \ --header 'authorization: Bearer {{auth-token}}' \ --header 'content-type: application/json' \ --data '{ "schema": { "name": "AzureHound", "display_name": "AzureHound", "version": "1.0.0", "namespace": "AZTEST" }, "node_kinds": [ { "name": "AZTEST_Tenant", "display_name": "Azure Tenant", "description": "An Azure tenant environment", "is_display_kind": true, "icon": "cloud", "color": "0xFF00FF" }, { "name": "AZTEST_Device", "display_name": "Azure Device", "description": "An Azure device", "is_display_kind": true, "icon": "desktop", "color": "0x0000FF" }, { "name": "AZTEST_User", "display_name": "Azure User", "description": "An Azure user account", "is_display_kind": true, "icon": "user", "color": "0x00FF00" }, { "name": "AZTEST_Group", "display_name": "Azure Group", "description": "An Azure security or distribution group", "is_display_kind": true, "icon": "group", "color": "0xFF0000" } ], "relationship_kinds": [ { "name": "AZTEST_MemberOf", "description": "User or computer is a member of a group", "is_traversable": false }, { "name": "AZTEST_HasSession", "description": "User has an active session on a device", "is_traversable": true } ], "environments": [ { "environment_kind": "AZTEST_Tenant", "source_kind": "AZBase", "principal_kinds": [ "AZTEST_User", "AZTEST_Group", "AZTEST_Device" ] } ], "relationship_findings": [ { "name": "AZTEST_GenericAll", "display_name": "Generic All Access (Azure)", "environment_kind": "AZTEST_Tenant", "relationship_kind": "AZTEST_HasSession", "source_kind": "AZBase", "remediation": { "short_description": "Principal has excessive permissions on target", "long_description": "This finding indicates that a principal has been granted GenericAll permissions on a target object, providing complete control including read, write, delete, and permission modification rights.", "short_remediation": "Remove the GenericAll permission from the principal'\''s access control list", "long_remediation": "# Remove Generic All Permissions\n\n## Steps to Remediate:\n\n1. **Identify the affected object**\n2. **Remove excessive permissions**\n3. **Grant minimal required permissions**" } } ] }'

  1. Deleted the extension with REST endpoint

curl --request DELETE \ --url http://bhe.localhost/api/v2/extensions/3 \ --header 'authorization: Bearer {{auth-token}}'

  1. Validated the extension and dependent data were deleted from the database

Screenshots (optional):

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist:

Summary by CodeRabbit

  • New Features

    • Added DELETE /api/v2/extensions/{extension_id} to remove graph schema extensions
    • GET /api/v2/extensions now requires the Open Graph Extension Management feature flag
  • Bug Fixes

    • Deletion blocks built-in extensions, runs in a transaction, and creates audit entries
    • Standardized error text to "id is malformed"
    • List extensions response now nests results under a data wrapper
  • Tests

    • Updated and added tests covering the new endpoint, response shape, and error-message changes

LawsonWillard and others added 27 commits January 27, 2026 19:09
# Conflicts:
#	cmd/api/src/database/migration/migrations/v8.6.0.sql
#	cmd/api/src/database/upsert_schema_extension_integration_test.go
#	cmd/api/src/model/graphschema.go
#	cmd/api/src/services/opengraphschema/extension.go
#	cmd/api/src/services/opengraphschema/extension_test.go
#	cmd/api/src/services/opengraphschema/mocks/opengraphschema.go
#	cmd/api/src/services/opengraphschema/opengraphschema.go
# Conflicts:
#	cmd/api/src/database/migration/migrations/v8.6.0.sql
# Conflicts:
#	cmd/api/src/model/graphschema.go
@kpowderly kpowderly self-assigned this Feb 3, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Adds a new extension ID URI constant, a DELETE /api/v2/extensions/{extension_id} API route and handler (feature-flag + auth), service and repository DeleteExtension wiring, DB transactional deletion with built-in protection and audit logging, updated mocks/tests, and normalized "id is malformed" error text.

Changes

Cohort / File(s) Summary
Constants & Errors
cmd/api/src/api/constant.go, cmd/api/src/api/error.go
Added URIPathVariableExtensionID constant; removed trailing period from ErrorResponseDetailsIDMalformed.
API Registration
cmd/api/src/api/registration/v2.go
Registered DELETE /api/v2/extensions/{extension_id} and added feature-flag check to GET /api/v2/extensions.
API Handlers & Mocks
cmd/api/src/api/v2/opengraphschema.go, cmd/api/src/api/v2/opengraphschema_test.go, cmd/api/src/api/v2/mocks/graphschemaextensions.go
Added Resources.DeleteExtension handler wired to new route (auth, feature-flag, path var parsing, error mapping); updated ListExtensions response usage; added tests and mock method for DeleteExtension.
Service Layer & Mocks
cmd/api/src/services/opengraphschema/extension.go, cmd/api/src/services/opengraphschema/extension_test.go, cmd/api/src/services/opengraphschema/opengraphschema.go, cmd/api/src/services/opengraphschema/mocks/opengraphschema.go
Added DeleteExtension on OpenGraphSchemaService that calls repository delete and refreshes graph kinds; added repository interface method DeleteGraphSchemaExtension; updated receiver naming and added service tests and repo mock.
Database Layer & Integration Test
cmd/api/src/database/graphschema.go, cmd/api/src/database/upsert_schema_extension.go, cmd/api/src/database/upsert_schema_extension_integration_test.go
DB deletion now runs in an auditable transaction, checks is_builtin (blocks built-ins), performs delete with audit entry; cleanup logic now always calls delete; integration test adjusted to assert built-in error.
Model / Audit
cmd/api/src/model/audit.go
Added AuditLogActionDeleteGraphSchemaExtension audit action constant.
Widespread Test Expectation Updates
cmd/api/src/api/v2/*_test.go, cmd/api/src/api/v2/fileingest_test.go, cmd/api/src/api/v2/saved_queries*.go
Updated many tests to expect "id is malformed" (removed trailing period) in error message assertions.

Sequence Diagram(s)

sequenceDiagram
    participant Client as HTTP Client
    participant Handler as DeleteExtension Handler
    participant Service as OpenGraphSchemaService
    participant Repo as OpenGraphSchemaRepository
    participant DB as Database

    Client->>Handler: DELETE /api/v2/extensions/{extension_id}
    Handler->>Handler: Check feature flag & RequireAuth (admin)
    Handler->>Handler: Parse extension_id
    Handler->>Service: DeleteExtension(ctx, extensionID)
    Service->>Repo: DeleteGraphSchemaExtension(ctx, extensionID)
    Repo->>DB: SELECT is_builtin FROM extensions WHERE id=...
    alt is_builtin = true
        DB-->>Repo: {is_builtin: true}
        Repo-->>Service: error (built-in)
        Service-->>Handler: wrapped error
        Handler-->>Client: HTTP 400/Conflict (error)
    else is_builtin = false
        DB-->>Repo: {is_builtin: false}
        Repo->>DB: DELETE FROM extensions WHERE id=...
        DB-->>Repo: rows affected
        Repo-->>Service: nil
        Service->>Repo: RefreshKinds()
        Repo-->>Service: nil
        Service-->>Handler: nil
        Handler-->>Client: HTTP 204 No Content
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • brandonshearin
  • LawsonWillard
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a Delete Extension endpoint. It references the ticket (BED-6898) and is appropriately specific without being overly verbose.
Description check ✅ Passed The PR description covers the key required sections: detailed description of changes, motivation/context with ticket reference, testing methodology (unit/integration and manual), and type of change. However, the checklist shows documentation (OpenAPI docs, code comments) is incomplete.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BED-6898

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@kpowderly kpowderly marked this pull request as ready for review February 6, 2026 19:24
@coderabbitai coderabbitai Bot added api A pull request containing changes affecting the API code. enhancement New feature or request labels Feb 6, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cmd/api/src/api/v2/opengraphschema.go`:
- Around line 305-310: The 500 path in the DeleteExtension handler leaks
internal error details by interpolating err into the response; update the else
branch that calls api.WriteErrorResponse / api.BuildErrorResponse to use the
generic api.ErrorResponseDetailsInternalServerError (the same pattern used in
OpenGraphSchemaIngest) instead of fmt.Sprintf("error deleting graph schema
extension: %v", err), leaving the database.ErrNotFound branch unchanged and
still returning 404 for s.OpenGraphSchemaService.DeleteExtension.

In `@cmd/api/src/services/opengraphschema/extension.go`:
- Around line 162-168: DeleteExtension currently removes the DB entry via
openGraphSchemaRepository.DeleteGraphSchemaExtension but does not refresh the
in-memory kinds map, risking stale schema state; update the method (rename
receiver from o to s) to call the service's kinds refresh routine immediately
after a successful delete (e.g., call s.refreshKinds() or the existing
reload/refresh method that updates the kinds map) and ensure any local variable
declarations are grouped in a var (...) block at the top of the function per
guidelines.
🧹 Nitpick comments (2)
cmd/api/src/database/graphschema.go (1)

208-231: Consider wrapping the SELECT + DELETE in a transaction for atomicity.

The existence/built-in check (Line 215) and the actual deletion (Line 227) are two separate queries without transactional protection. A concurrent request could delete the same extension between the two queries, causing the DELETE to return ErrNotFound even though the first check passed. The impact is benign (the caller sees "not found"), but wrapping both in a transaction would make the behavior more predictable and consistent.

Also, given that CreateGraphSchemaExtension uses AuditableTransaction for audit logging, consider whether deleting an extension should also be audited—extension deletion is a destructive, admin-only action.

cmd/api/src/api/v2/opengraphschema.go (1)

303-304: Inconsistent use of request.Context() vs the local ctx variable.

Line 304 calls request.Context() directly, while every other branch in this function (and the ctx declaration at line 294) uses the local ctx. Use ctx here for consistency.

Proposed fix
 	} else if extID, err := strconv.ParseInt(extensionID, 10, 32); err != nil {
-		api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response)
+		api.WriteErrorResponse(ctx, api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response)

Comment thread cmd/api/src/api/v2/opengraphschema.go
Comment thread cmd/api/src/services/opengraphschema/extension.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cmd/api/src/api/v2/opengraphschema.go`:
- Around line 305-310: The delete handler currently only checks for
database.ErrNotFound and returns 500 for built-in-extension errors; update the
error branch that handles s.OpenGraphSchemaService.DeleteExtension to also
detect errors.Is(err, model.ErrGraphExtensionBuiltIn) and return a 400 using
api.WriteErrorResponse(api.BuildErrorResponse(http.StatusBadRequest, ...)),
mirroring the upsert handler behavior; ensure you import/reference
model.ErrGraphExtensionBuiltIn (and keep the existing database.ErrNotFound
check) so attempts to delete built-in extensions return a 400 Bad Request
instead of 500.

In `@cmd/api/src/database/graphschema.go`:
- Around line 221-243: The SELECT and DELETE inside the AuditableTransaction
callback are using s.db.WithContext(ctx) and thus bypass the transaction; change
those calls to use the transaction variable (tx) instead — e.g. replace
s.db.WithContext(ctx).Raw(...) with tx.WithContext(ctx).Raw(fmt.Sprintf(`SELECT
is_builtin FROM %s WHERE id = ?`, schemaExtension.TableName()), extensionId) and
replace s.db.WithContext(ctx).Exec(...) with
tx.WithContext(ctx).Exec(fmt.Sprintf(`DELETE FROM %s WHERE id = ?`,
schemaExtension.TableName()), extensionId), keeping the existing CheckError,
ErrNotFound and model.ErrGraphExtensionBuiltIn handling so the SELECT,
is_builtin check and DELETE all participate in AuditableTransaction.

Comment thread cmd/api/src/api/v2/opengraphschema.go
Comment thread cmd/api/src/database/graphschema.go
}

api.WriteJSONResponse(ctx, ExtensionsResponse{Extensions: extensionsResponse}, http.StatusOK, response)
api.WriteBasicResponse(ctx, ExtensionsResponse{Extensions: extensionsResponse}, http.StatusOK, response)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nice catch

Copy link
Copy Markdown
Contributor

@brandonshearin brandonshearin left a comment

Choose a reason for hiding this comment

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

nice-- well tested as always.

@kpowderly kpowderly merged commit ccafd15 into main Feb 10, 2026
14 checks passed
@kpowderly kpowderly deleted the BED-6898 branch February 10, 2026 19:55
@github-actions github-actions Bot locked and limited conversation to collaborators Feb 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

api A pull request containing changes affecting the API code. enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants