Skip to content

feat(bridge): add bridges REST API, admin UI, and CLI enrollment - #3698

Open
ryanmelt wants to merge 1 commit into
pr4-interface-wiringfrom
pr5-api-ui-cli
Open

feat(bridge): add bridges REST API, admin UI, and CLI enrollment#3698
ryanmelt wants to merge 1 commit into
pr4-interface-wiringfrom
pr5-api-ui-cli

Conversation

@ryanmelt

Copy link
Copy Markdown
Member

Stacked PR 5 of 5 — base: pr4-interface-wiring (#3697). Final part of the #3688 split.

  • bridges_controller.rb + routes.rb — Bridges REST API
  • BridgesTab.vue + admin/tabs/index.js — Admin UI tab
  • bin/openc3clibridgeenroll command

Reviewer focus

Rails + frontend only (no Python/Docker). Functionally this depends only on the models (PR 2); it's stacked on PR 4 purely for linear ordering.

Review after #3697.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 20.31250% with 51 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.26%. Comparing base (e4bf403) to head (a4a5883).

Files with missing lines Patch % Lines
...-cmd-tlm-api/app/controllers/bridges_controller.rb 20.63% 50 Missing ⚠️
...es/openc3-vue-common/src/tools/admin/tabs/index.js 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                    @@
##           pr4-interface-wiring    #3698      +/-   ##
========================================================
- Coverage                 79.28%   79.26%   -0.03%     
========================================================
  Files                       887      888       +1     
  Lines                     65511    65575      +64     
  Branches                   2537     2585      +48     
========================================================
+ Hits                      51941    51976      +35     
- Misses                    12897    12929      +32     
+ Partials                    673      670       -3     
Flag Coverage Δ
frontend 63.60% <0.00%> (+0.08%) ⬆️
python 81.53% <ø> (ø)
ruby-api 81.60% <20.63%> (-0.58%) ⬇️
ruby-backend 84.05% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@ryan-pratt

Copy link
Copy Markdown
Contributor

Created a stack to make this easier to navigate

return
end
shard = OpenC3::ScopeModel.get_model(name: params[:scope])&.shard || 0
OpenC3::BridgeModel.build_microservice(bridge_name: name, scope: params[:scope], shard: shard).create

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.

Same comment about deploy() as I had on the migration in the previous PR... Am I just missing where these get deployed?

@jmthomas jmthomas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  1. Read-only system role can steal a bridge enrollment → host code execution
    BridgesController inherits index/show from ModelController, which gate on authorization('system'), and BridgeModel#as_json (PR2) returns ticket, enroll_code, and public_key verbatim. So any user with the read-only system scope can:

GET /openc3-api/bridges/all → {"LAB1": {"ticket": "...", "enroll_code": "...", ...}}

ticket is the dial capability for the hub and enroll_code is a live one-time enrollment code. Together they let that user enroll their own openc3-app as the bridge's authorized control identity, which grants spawning host-side interfaces on the operator's machine. Minting the code requires admin; reading it does not. enroll_code also has no expiry — generate_enrollment_token sets it and nothing ever clears an unredeemed one, so the window is unbounded.

Fix: override index/show with authorization('admin') and redact — the tab only consumes name, app_public_key, and truthiness of ticket, so serve {name, app_public_key, reachable: !ticket.nil?} and never ship enroll_code/ticket to a browser at all. Separately, expire enroll_code (timestamp + TTL check at redemption).

  1. A bridge whose hub never starts is invisible and unrecoverable in the UI
    create writes only a MicroserviceModel; the BridgeModel record is written later by the running hub (_ensure_keys). But the tab lists BridgeModel.all (/bridges/all). If the operator never schedules the hub — wrong shard, crash loop, image problem — then:
  • the bridge never renders in the tab (empty list, no indication anything is pending)
  • create keeps returning 409 "Bridge 'X' already exists" forever, because the conflict check is against MicroserviceModel (bridges_controller.rb:44)
  • there is no row, so no delete button

Fix: build the list from the #{scope}BRIDGE* microservice names left-joined onto BridgeModel, so a pending/failed hub renders as STARTING/DOWN with a working delete.

# over the local Docker control plane).
def token
return unless authorization('admin')
model = @model_class.get_model(name: params[:id], scope: params[:scope])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add name = params[:id].to_s.upcase like destroy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Better yet add one before_action that canonicalizes params[:id], and .upcase the CLI arg. Because otherwise the show action will fail with lowercase

begin
OpenC3::Secrets.getClient.delete("BRIDGE_#{name}_PRIVATE_KEY", scope: scope)
rescue StandardError
# ignore: nothing to delete

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might be worth a Logger.warn since redis issue or permission error will strand the PRIVATE_KEY. Are we expecting this to be called with "nothing to delete"?

Comment thread openc3/bin/openc3cli
model.app_public_key = app_public_key
model.update
# Print ONLY the ticket so openc3-app can capture it from stdout.
puts model.ticket

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

bridgeenroll changes an authorization with no validation and no audit trail. app_public_key is accepted as any non-empty string and silently overwrites an existing enrollment. Every controller action logs OpenC3::Logger.info(..., user: username()); re-pointing a bridge's authorized control identity logs nothing. A typo'd key locks out the real openc3-app with no record of what changed.

Fix: validate /\A[0-9a-f]{64}\z/i and Logger.info the old→new key.

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.

3 participants