Skip to content

Commit a33349c

Browse files
authored
feat: use oauth client credentials (#166)
1 parent c8717e0 commit a33349c

File tree

3 files changed

+297
-40
lines changed

3 files changed

+297
-40
lines changed

agent/config/types.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ type GitManager struct {
3535

3636
// FleetManager represents the Orb ConfigManager configuration.
3737
type FleetManager struct {
38-
URL string `yaml:"url"`
39-
TokenURL string `yaml:"token_url"`
40-
ClientID string `yaml:"client_id"`
41-
ClientSecret string `yaml:"client_secret"`
42-
TopicName string `yaml:"topic_name"`
38+
URL string `yaml:"url"`
39+
TokenURL string `yaml:"token_url"`
40+
ClientID string `yaml:"client_id"`
41+
ClientSecret string `yaml:"client_secret"`
42+
MQTTURL string `yaml:"mqtt_url,omitempty"`
43+
HeartbeatTopic string `yaml:"heartbeat_topic,omitempty"`
44+
CapabilitiesTopic string `yaml:"capabilities_topic,omitempty"`
45+
// TopicName is kept for backward compatibility
46+
TopicName string `yaml:"topic_name,omitempty"`
4347
}
4448

4549
// Sources represents the configuration for manager sources, including cloud, local and git.

agent/configmgr/fleet.go

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"context"
66
"crypto/tls"
7-
"encoding/base64"
87
"encoding/json"
98
"fmt"
109
"io"
@@ -104,20 +103,76 @@ func newFleetConfigManager(ctx context.Context, logger *slog.Logger, pMgr policy
104103
}
105104

106105
func (fleetManager *fleetConfigManager) Start(cfg config.Config, backends map[string]backend.Backend) error {
106+
ctx, cancel := context.WithCancel(context.Background())
107+
defer cancel()
108+
107109
// call the token url to get the token
108-
token, err := fleetManager.getToken(cfg.OrbAgent.ConfigManager.Sources.Fleet.TokenURL, cfg.OrbAgent.ConfigManager.Sources.Fleet.ClientID, cfg.OrbAgent.ConfigManager.Sources.Fleet.ClientSecret)
110+
token, err := fleetManager.getToken(ctx, cfg.OrbAgent.ConfigManager.Sources.Fleet.TokenURL, cfg.OrbAgent.ConfigManager.Sources.Fleet.ClientID, cfg.OrbAgent.ConfigManager.Sources.Fleet.ClientSecret)
109111
if err != nil {
110112
return err
111113
}
112114

113-
// use the token to connect over MQTT v5
114-
err = fleetManager.connect(token.MQTTURL, token.AccessToken, token.Topics, backends)
115+
// merge configuration values with token response values (config takes priority)
116+
mqttURL, topics := fleetManager.mergeConfigWithTokenResponse(cfg.OrbAgent.ConfigManager.Sources.Fleet, token)
117+
118+
// use the merged configuration to connect over MQTT v5
119+
err = fleetManager.connect(mqttURL, token.AccessToken, topics, backends)
115120
if err != nil {
116121
return err
117122
}
118123
return nil
119124
}
120125

126+
// mergeConfigWithTokenResponse merges configuration values with token response values,
127+
// giving priority to token response values when they are provided
128+
func (fleetManager *fleetConfigManager) mergeConfigWithTokenResponse(fleetCfg config.FleetManager, token *tokenResponse) (string, tokenResponseTopics) {
129+
// Start with configuration values as defaults
130+
mqttURL := fleetCfg.MQTTURL
131+
topics := tokenResponseTopics{
132+
Heartbeat: fleetCfg.HeartbeatTopic,
133+
Capabilities: fleetCfg.CapabilitiesTopic,
134+
}
135+
136+
// Handle legacy TopicName field for backward compatibility - only if specific topics aren't set
137+
if fleetCfg.TopicName != "" && fleetCfg.HeartbeatTopic == "" && fleetCfg.CapabilitiesTopic == "" {
138+
fleetManager.logger.Debug("using legacy topic name as base for heartbeat and capabilities", "topic", fleetCfg.TopicName)
139+
topics.Heartbeat = fleetCfg.TopicName + "/heartbeat"
140+
topics.Capabilities = fleetCfg.TopicName + "/capabilities"
141+
}
142+
143+
// Override with token response values if provided (token takes priority)
144+
if token.MQTTURL != "" {
145+
fleetManager.logger.Debug("using MQTT URL from token response", "token_url", token.MQTTURL, "config_url", fleetCfg.MQTTURL)
146+
mqttURL = token.MQTTURL
147+
} else if mqttURL != "" {
148+
fleetManager.logger.Debug("using MQTT URL from configuration", "config_url", mqttURL)
149+
}
150+
151+
// Token response topics override configuration topics
152+
if token.Topics.Heartbeat != "" {
153+
fleetManager.logger.Debug("using heartbeat topic from token response", "token_topic", token.Topics.Heartbeat, "config_topic", topics.Heartbeat)
154+
topics.Heartbeat = token.Topics.Heartbeat
155+
}
156+
157+
if token.Topics.Capabilities != "" {
158+
fleetManager.logger.Debug("using capabilities topic from token response", "token_topic", token.Topics.Capabilities, "config_topic", topics.Capabilities)
159+
topics.Capabilities = token.Topics.Capabilities
160+
}
161+
162+
// Token response always provides inbox/outbox topics
163+
topics.Inbox = token.Topics.Inbox
164+
topics.Outbox = token.Topics.Outbox
165+
166+
fleetManager.logger.Info("merged configuration and token response",
167+
"mqtt_url", mqttURL,
168+
"heartbeat_topic", topics.Heartbeat,
169+
"capabilities_topic", topics.Capabilities,
170+
"inbox_topic", topics.Inbox,
171+
"outbox_topic", topics.Outbox)
172+
173+
return mqttURL, topics
174+
}
175+
121176
func (fleetManager *fleetConfigManager) connectWithContext(ctx context.Context, fleetMQTTURL, token string, topics tokenResponseTopics, backends map[string]backend.Backend) error {
122177
// Parse the ORB URL
123178
serverURL, err := url.Parse(fleetMQTTURL)
@@ -314,64 +369,94 @@ type tokenResponse struct {
314369
ExpiresIn int `json:"expires_in"`
315370
}
316371

317-
// getTokenWithContext is the internal implementation that obeys the supplied
318-
// context for cancellation. It was introduced so that production code can be
319-
// context-aware while preserving the original test helper signature.
320-
func (fleetManager *fleetConfigManager) getTokenWithContext(ctx context.Context, tokenURL string, clientID string, clientSecret string) (*tokenResponse, error) {
372+
func (fleetManager *fleetConfigManager) getToken(ctx context.Context, tokenURL string, clientID string, clientSecret string) (*tokenResponse, error) {
373+
// Input validation
374+
if tokenURL == "" {
375+
return nil, fmt.Errorf("token URL cannot be empty")
376+
}
377+
if clientID == "" {
378+
return nil, fmt.Errorf("client ID cannot be empty")
379+
}
380+
if clientSecret == "" {
381+
return nil, fmt.Errorf("client secret cannot be empty")
382+
}
383+
384+
fleetManager.logger.Debug("requesting access token", "token_url", tokenURL, "client_id", clientID)
385+
321386
scopes := []string{
322-
"rabbitmq.read:*/*",
323-
"rabbitmq.write:*/*",
324-
"rabbitmq.configure:*/*",
387+
"orb.read:*/*/*",
388+
"orb.write:*/*/*",
389+
"orb.configure:*/*/*",
325390
}
326391

327392
data := url.Values{}
328393
data.Set("grant_type", "client_credentials")
329394
data.Set("scope", strings.Join(scopes, " "))
395+
data.Set("client_id", clientID)
396+
data.Set("client_secret", clientSecret)
330397

331-
// Encode credentials in Basic Auth header
332-
creds := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", clientID, clientSecret)))
398+
fleetManager.logger.Debug("sending token request", "url", tokenURL, "data", data, "client_id", clientID) //, "client_secret", clientSecret)
333399

334400
req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(data.Encode()))
335401
if err != nil {
402+
fleetManager.logger.Error("failed to create token request", "error", err, "token_url", tokenURL)
336403
return nil, fmt.Errorf("failed to create request: %w", err)
337404
}
338405
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
339-
req.Header.Set("Authorization", "Basic "+creds)
340406

341-
// HTTP client with TLS verification disabled
407+
// HTTP client with configurable timeout and TLS settings
342408
httpClient := &http.Client{
409+
Timeout: 30 * time.Second, // TODO: make configurable
343410
Transport: &http.Transport{
344-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // TODO: make configurable?
411+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // TODO: make configurable
345412
},
346413
}
347414

415+
fleetManager.logger.Debug("sending token request", "url", tokenURL)
348416
resp, err := httpClient.Do(req.WithContext(ctx))
349417
if err != nil {
350-
return nil, fmt.Errorf("failed to send request: %w", err)
418+
fleetManager.logger.Error("failed to send token request", "error", err, "token_url", tokenURL)
419+
return nil, fmt.Errorf("failed to send request to %s: %w", tokenURL, err)
351420
}
352421
defer func() {
353422
if err := resp.Body.Close(); err != nil {
354423
fleetManager.logger.Error("failed to close response body", "error", err)
355424
}
356425
}()
357426

358-
body, _ := io.ReadAll(resp.Body)
427+
body, err := io.ReadAll(resp.Body)
428+
if err != nil {
429+
fleetManager.logger.Error("failed to read response body", "error", err, "status_code", resp.StatusCode)
430+
return nil, fmt.Errorf("failed to read response body: %w", err)
431+
}
432+
359433
if resp.StatusCode != 200 {
360-
return nil, fmt.Errorf("token request failed: %s", body)
434+
fleetManager.logger.Error("token request failed",
435+
"status_code", resp.StatusCode,
436+
"response", string(body),
437+
"token_url", tokenURL,
438+
"client_id", clientID)
439+
return nil, fmt.Errorf("token request failed with status %d: %s", resp.StatusCode, string(body))
361440
}
362441

363442
var tokenResponse tokenResponse
364443
if err := json.Unmarshal(body, &tokenResponse); err != nil {
444+
fleetManager.logger.Error("failed to parse token response", "error", err, "response", string(body))
365445
return nil, fmt.Errorf("failed to parse token response: %w", err)
366446
}
367447

368-
return &tokenResponse, nil
369-
}
448+
// Validate token response
449+
if tokenResponse.AccessToken == "" {
450+
fleetManager.logger.Error("received empty access token", "response", string(body))
451+
return nil, fmt.Errorf("received empty access token from server")
452+
}
453+
454+
fleetManager.logger.Info("successfully obtained access token",
455+
"token_url", tokenURL,
456+
"expires_in", tokenResponse.ExpiresIn,
457+
"mqtt_url", tokenResponse.MQTTURL)
370458

371-
// getToken is kept for backward-compatibility with existing tests. It delegates
372-
// to getTokenWithContext using the fleet manager’s root context.
373-
func (fleetManager *fleetConfigManager) getToken(tokenURL string, clientID string, clientSecret string) (*tokenResponse, error) {
374-
return fleetManager.getTokenWithContext(fleetManager.heartbeater.heartbeatCtx, tokenURL, clientID, clientSecret)
459+
return &tokenResponse, nil
375460
}
376461

377462
func (fleetManager *fleetConfigManager) GetContext(ctx context.Context) context.Context {

0 commit comments

Comments
 (0)