Merge pull request #981 from Molecule-AI/fix/security-tenant-cpprovisioner-bearer
fix(security): tenant CPProvisioner sends CP bearer on provision / stop / status
This commit is contained in:
commit
ea5cb88183
@ -18,9 +18,10 @@ import (
|
||||
//
|
||||
// Auto-activated when MOLECULE_ORG_ID is set (SaaS tenant).
|
||||
type CPProvisioner struct {
|
||||
baseURL string
|
||||
orgID string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
orgID string
|
||||
sharedSecret string // bearer passed to CP's /cp/workspaces/* gate
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewCPProvisioner creates a provisioner that delegates to the control plane.
|
||||
@ -39,13 +40,33 @@ func NewCPProvisioner() (*CPProvisioner, error) {
|
||||
baseURL = "https://api.moleculesai.app"
|
||||
}
|
||||
|
||||
// CP gates /cp/workspaces/* behind a bearer check (C1). Without the
|
||||
// shared secret the CP returns 401 — or 404 if the routes refused
|
||||
// to mount on its side. Tenant operators should set this on the
|
||||
// tenant env to the same value as the CP's PROVISION_SHARED_SECRET.
|
||||
sharedSecret := os.Getenv("MOLECULE_CP_SHARED_SECRET")
|
||||
if sharedSecret == "" {
|
||||
// Fall back to PROVISION_SHARED_SECRET so a single env-var name
|
||||
// works on both sides of the wire.
|
||||
sharedSecret = os.Getenv("PROVISION_SHARED_SECRET")
|
||||
}
|
||||
|
||||
return &CPProvisioner{
|
||||
baseURL: baseURL,
|
||||
orgID: orgID,
|
||||
httpClient: &http.Client{Timeout: 120 * time.Second},
|
||||
baseURL: baseURL,
|
||||
orgID: orgID,
|
||||
sharedSecret: sharedSecret,
|
||||
httpClient: &http.Client{Timeout: 120 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// authHeader sets Authorization: Bearer on the outbound request. No-op
|
||||
// when sharedSecret is empty so self-hosted / dev deployments still work.
|
||||
func (p *CPProvisioner) authHeader(req *http.Request) {
|
||||
if p.sharedSecret != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+p.sharedSecret)
|
||||
}
|
||||
}
|
||||
|
||||
type cpProvisionRequest struct {
|
||||
OrgID string `json:"org_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
@ -84,6 +105,7 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
|
||||
return "", fmt.Errorf("cp provisioner: create request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
p.authHeader(httpReq)
|
||||
|
||||
resp, err := p.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
@ -118,6 +140,7 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
|
||||
func (p *CPProvisioner) Stop(ctx context.Context, workspaceID string) error {
|
||||
url := fmt.Sprintf("%s/cp/workspaces/%s?instance_id=%s", p.baseURL, workspaceID, workspaceID)
|
||||
req, _ := http.NewRequestWithContext(ctx, "DELETE", url, nil)
|
||||
p.authHeader(req)
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cp provisioner: stop: %w", err)
|
||||
@ -130,6 +153,7 @@ func (p *CPProvisioner) Stop(ctx context.Context, workspaceID string) error {
|
||||
func (p *CPProvisioner) IsRunning(ctx context.Context, workspaceID string) (bool, error) {
|
||||
url := fmt.Sprintf("%s/cp/workspaces/%s/status?instance_id=%s", p.baseURL, workspaceID, workspaceID)
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
p.authHeader(req)
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
Loading…
Reference in New Issue
Block a user