feat(bridge): add bridges REST API, admin UI, and CLI enrollment - #3698
feat(bridge): add bridges REST API, admin UI, and CLI enrollment#3698ryanmelt wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ae9b4d9 to
e07d6a2
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e07d6a2 to
a4a5883
Compare
|
|
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 |
There was a problem hiding this comment.
Same comment about deploy() as I had on the migration in the previous PR... Am I just missing where these get deployed?
jmthomas
left a comment
There was a problem hiding this comment.
- 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).
- 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]) |
There was a problem hiding this comment.
Add name = params[:id].to_s.upcase like destroy
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"?
| model.app_public_key = app_public_key | ||
| model.update | ||
| # Print ONLY the ticket so openc3-app can capture it from stdout. | ||
| puts model.ticket |
There was a problem hiding this comment.
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.



Stacked PR 5 of 5 — base:
pr4-interface-wiring(#3697). Final part of the #3688 split.bridges_controller.rb+routes.rb— Bridges REST APIBridgesTab.vue+admin/tabs/index.js— Admin UI tabbin/openc3cli—bridgeenrollcommandReviewer 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.
🤖 Generated with Claude Code