feat(bridge): add bridges REST API, admin UI, and CLI enrollment - #3698
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## pr4-interface-wiring #3698 +/- ##
========================================================
+ Coverage 79.12% 79.14% +0.01%
========================================================
Files 893 894 +1
Lines 66751 66857 +106
Branches 2591 2591
========================================================
+ Hits 52820 52911 +91
- Misses 13271 13286 +15
Partials 660 660
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?
There was a problem hiding this comment.
Deploy was missing. Deploy primarily copies bundled files for microservices to the buckets, which this has none, so it didn't really matter... but still incorrect so I added it.
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.
There was a problem hiding this comment.
No Logger as this is just from the Command Line and there is not associated username which would be the main point of the log. Added validation checks though.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
01e3c76 to
8581995
Compare
|



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