Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ type Cloud struct {
Secret string
LocationSecret string // Deprecated: Use LocationSecrets
LocationSecrets []LocationSecret
APIKey APIKey
LocationID string
PrimaryOrgID string
MachineID string
Expand Down Expand Up @@ -588,6 +589,7 @@ type cloudData struct {

LocationSecret string `json:"location_secret"`
LocationSecrets []LocationSecret `json:"location_secrets"`
APIKey APIKey `json:"api_key"`
LocationID string `json:"location_id"`
PrimaryOrgID string `json:"primary_org_id"`
MachineID string `json:"machine_id"`
Expand All @@ -603,6 +605,33 @@ type cloudData struct {
TLSPrivateKey string `json:"tls_private_key"`
}

// APIKey is the cloud app authentication credential
type APIKey struct {
ID string `json:"id"`
Key string `json:"key"`
}

// IsFullySet returns true if an APIKey has both the ID and Key fields set.
func (a APIKey) IsFullySet() bool {
return a.ID != "" && a.Key != ""
}

// IsPartiallySet returns true if only one of the ID or Key fields are set.
func (a APIKey) IsPartiallySet() bool {
return (a.ID == "" && a.Key != "") || (a.ID != "" && a.Key == "")
}

// GetCloudCredsDialOpt returns a dial option with the cloud credentials for this cloud config.
// API keys are always preferred over robot secrets. If neither are set, nil is returned.
func (config *Cloud) GetCloudCredsDialOpt() rpc.DialOption {
if config.APIKey.IsFullySet() {
return rpc.WithEntityCredentials(config.APIKey.ID, rpc.Credentials{rutils.CredentialsTypeAPIKey, config.APIKey.Key})
} else if config.Secret != "" {
return rpc.WithEntityCredentials(config.ID, rpc.Credentials{rutils.CredentialsTypeRobotSecret, config.Secret})
}
return nil
}

// UnmarshalJSON unmarshals JSON data into this config.
func (config *Cloud) UnmarshalJSON(data []byte) error {
var temp cloudData
Expand All @@ -614,6 +643,7 @@ func (config *Cloud) UnmarshalJSON(data []byte) error {
Secret: temp.Secret,
LocationSecret: temp.LocationSecret,
LocationSecrets: temp.LocationSecrets,
APIKey: temp.APIKey,
LocationID: temp.LocationID,
PrimaryOrgID: temp.PrimaryOrgID,
MachineID: temp.MachineID,
Expand Down Expand Up @@ -643,6 +673,7 @@ func (config Cloud) MarshalJSON() ([]byte, error) {
Secret: config.Secret,
LocationSecret: config.LocationSecret,
LocationSecrets: config.LocationSecrets,
APIKey: config.APIKey,
LocationID: config.LocationID,
PrimaryOrgID: config.PrimaryOrgID,
MachineID: config.MachineID,
Expand Down Expand Up @@ -673,8 +704,10 @@ func (config *Cloud) Validate(path string, fromCloud bool) error {
if config.LocalFQDN == "" {
return resource.NewConfigValidationFieldRequiredError(path, "local_fqdn")
}
} else if config.Secret == "" {
return resource.NewConfigValidationFieldRequiredError(path, "secret")
} else if config.APIKey.IsPartiallySet() {
return resource.NewConfigValidationFieldRequiredError(path, "api_key")
} else if config.Secret == "" && !config.APIKey.IsFullySet() {
return resource.NewConfigValidationFieldRequiredError(path, "api_key")
}
if config.RefreshInterval == 0 {
config.RefreshInterval = 10 * time.Second
Expand Down Expand Up @@ -1060,6 +1093,7 @@ func CreateTLSWithCert(cfg *Config) (*tls.Config, error) {
func ProcessConfig(in *Config) (*Config, error) {
out := *in
var selfCreds *rpc.Credentials
var selfAuthEntity string
if in.Cloud != nil {
// We expect a cloud config from app to always contain a non-empty `TLSCertificate` field.
// We do this empty string check just to cope with unexpected input, such as cached configs
Expand All @@ -1071,7 +1105,13 @@ func ProcessConfig(in *Config) (*Config, error) {
}
out.Network.TLSConfig = tlsConfig
}
selfCreds = &rpc.Credentials{rutils.CredentialsTypeRobotSecret, in.Cloud.Secret}
if in.Cloud.APIKey.IsFullySet() {
selfCreds = &rpc.Credentials{rutils.CredentialsTypeAPIKey, in.Cloud.APIKey.Key}
selfAuthEntity = in.Cloud.APIKey.ID
} else {
selfCreds = &rpc.Credentials{rutils.CredentialsTypeRobotSecret, in.Cloud.Secret}
selfAuthEntity = in.Cloud.ID
}
}

out.Remotes = make([]Remote, len(in.Remotes))
Expand All @@ -1086,7 +1126,7 @@ func ProcessConfig(in *Config) (*Config, error) {
}
remoteCopy.Auth.Managed = true
remoteCopy.Auth.SignalingServerAddress = in.Cloud.SignalingAddress
remoteCopy.Auth.SignalingAuthEntity = in.Cloud.ID
remoteCopy.Auth.SignalingAuthEntity = selfAuthEntity
Copy link
Member

Choose a reason for hiding this comment

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

what is this used for?

Copy link
Member Author

@danielbotros danielbotros Jan 6, 2026

Choose a reason for hiding this comment

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

It's used to authenticate to the signaling server for creating a WebRTC connection to a remote robot I believe

remoteCopy.Auth.SignalingCreds = selfCreds
}
out.Remotes[idx] = remoteCopy
Expand Down
24 changes: 22 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,24 @@ func TestConfigEnsure(t *testing.T) {
invalidCloud.Cloud.ID = "some_id"
err = invalidCloud.Ensure(false, logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "secret")
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "api_key")
err = invalidCloud.Ensure(true, logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "fqdn")
invalidCloud.Cloud.Secret = "my_secret"
test.That(t, invalidCloud.Ensure(false, logger), test.ShouldBeNil)
test.That(t, invalidCloud.Ensure(true, logger), test.ShouldNotBeNil)
invalidCloud.Cloud.APIKey = config.APIKey{ID: "", Key: "key_value"}
err = invalidCloud.Ensure(false, logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "api_key")
err = invalidCloud.Ensure(true, logger)
test.That(t, err, test.ShouldNotBeNil)
invalidCloud.Cloud.Secret = ""
invalidCloud.Cloud.APIKey = config.APIKey{ID: "key_id", Key: "key_value"}
test.That(t, invalidCloud.Ensure(false, logger), test.ShouldBeNil)
test.That(t, invalidCloud.Ensure(true, logger), test.ShouldNotBeNil)
invalidCloud.Cloud.APIKey = config.APIKey{}
invalidCloud.Cloud.FQDN = "wooself"
err = invalidCloud.Ensure(true, logger)
test.That(t, err, test.ShouldNotBeNil)
Expand Down Expand Up @@ -475,14 +485,24 @@ func TestConfigEnsurePartialStart(t *testing.T) {
invalidCloud.Cloud.ID = "some_id"
err = invalidCloud.Ensure(false, logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "secret")
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "api_key")
err = invalidCloud.Ensure(true, logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "fqdn")
invalidCloud.Cloud.Secret = "my_secret"
test.That(t, invalidCloud.Ensure(false, logger), test.ShouldBeNil)
test.That(t, invalidCloud.Ensure(true, logger), test.ShouldNotBeNil)
invalidCloud.Cloud.APIKey = config.APIKey{ID: "", Key: "key_value"}
err = invalidCloud.Ensure(false, logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "api_key")
err = invalidCloud.Ensure(true, logger)
test.That(t, err, test.ShouldNotBeNil)
invalidCloud.Cloud.Secret = ""
invalidCloud.Cloud.APIKey = config.APIKey{ID: "key_id", Key: "key_value"}
test.That(t, invalidCloud.Ensure(false, logger), test.ShouldBeNil)
test.That(t, invalidCloud.Ensure(true, logger), test.ShouldNotBeNil)
invalidCloud.Cloud.APIKey = config.APIKey{}
invalidCloud.Cloud.FQDN = "wooself"
err = invalidCloud.Ensure(true, logger)
test.That(t, err, test.ShouldNotBeNil)
Expand Down
3 changes: 3 additions & 0 deletions config/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ func prettyDiff(left, right Config) (string, error) {
conf.Cloud.LocationSecrets[i].Secret = mask
}
}
if conf.Cloud.APIKey.Key != "" {
conf.Cloud.APIKey.Key = mask
}
Comment on lines +148 to +150
Copy link
Member

Choose a reason for hiding this comment

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

why just compare key and not id?

Copy link
Member Author

Choose a reason for hiding this comment

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

Key is a secret so we want to mask it out while ID isn't but I think it's also fine to mask out the ID if you prefer

// Not really a secret but annoying to diff
if conf.Cloud.TLSCertificate != "" {
conf.Cloud.TLSCertificate = mask
Expand Down
5 changes: 5 additions & 0 deletions config/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,10 @@ func TestDiffSanitize(t *testing.T) {
{ID: "id1", Secret: "sec1"},
{ID: "id2", Secret: "sec2"},
},
APIKey: config.APIKey{
ID: "api_key_id",
Key: "sec3",
},
TLSCertificate: "foo",
TLSPrivateKey: "bar",
}
Expand Down Expand Up @@ -675,6 +679,7 @@ func TestDiffSanitize(t *testing.T) {
test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.LocationSecret)
test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.LocationSecrets[0].Secret)
test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.LocationSecrets[1].Secret)
test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.APIKey.Key)
test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.TLSCertificate)
test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.TLSPrivateKey)
for _, hdlr := range auth1.Handlers {
Expand Down
14 changes: 6 additions & 8 deletions config/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,14 +730,12 @@ func CreateNewGRPCClient(ctx context.Context, cloudCfg *Cloud, logger logging.Lo
}

dialOpts := make([]rpc.DialOption, 0, 2)
// Only add credentials when secret is set.
if cloudCfg.Secret != "" {
dialOpts = append(dialOpts, rpc.WithEntityCredentials(cloudCfg.ID,
rpc.Credentials{
Type: rutils.CredentialsTypeRobotSecret,
Payload: cloudCfg.Secret,
},
))

cloudCreds := cloudCfg.GetCloudCredsDialOpt()

// Only add credentials when they are set.
if cloudCreds != nil {
dialOpts = append(dialOpts, cloudCreds)
}

if u.Scheme == "http" {
Expand Down
8 changes: 4 additions & 4 deletions config/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestFromReader(t *testing.T) {
fakeServer.StoreDeviceConfig(robotPartID, protoConfig, certProto)

appAddress := fmt.Sprintf("http://%s", fakeServer.Addr().String())
appConn, err := grpc.NewAppConn(ctx, appAddress, secret, robotPartID, logger)
appConn, err := grpc.NewAppConn(ctx, appAddress, robotPartID, cloudResponse.GetCloudCredsDialOpt(), logger)
test.That(t, err, test.ShouldBeNil)
defer appConn.Close()
cfgText := fmt.Sprintf(`{"cloud":{"id":%q,"app_address":%q,"secret":%q}}`, robotPartID, appAddress, secret)
Expand Down Expand Up @@ -120,7 +120,7 @@ func TestFromReader(t *testing.T) {
fakeServer.StoreDeviceConfig(robotPartID, nil, nil)

appAddress := fmt.Sprintf("http://%s", fakeServer.Addr().String())
appConn, err := grpc.NewAppConn(ctx, appAddress, secret, robotPartID, logger)
appConn, err := grpc.NewAppConn(ctx, appAddress, robotPartID, cachedCloud.GetCloudCredsDialOpt(), logger)
test.That(t, err, test.ShouldBeNil)
defer appConn.Close()
cfgText := fmt.Sprintf(`{"cloud":{"id":%q,"app_address":%q,"secret":%q}}`, robotPartID, appAddress, secret)
Expand Down Expand Up @@ -162,7 +162,7 @@ func TestFromReader(t *testing.T) {
fakeServer.StoreDeviceConfig(robotPartID, protoConfig, certProto)

appAddress := fmt.Sprintf("http://%s", fakeServer.Addr().String())
appConn, err := grpc.NewAppConn(ctx, appAddress, secret, robotPartID, logger)
appConn, err := grpc.NewAppConn(ctx, appAddress, robotPartID, cloudResponse.GetCloudCredsDialOpt(), logger)
test.That(t, err, test.ShouldBeNil)
defer appConn.Close()
cfgText := fmt.Sprintf(`{"cloud":{"id":%q,"app_address":%q,"secret":%q}}`, robotPartID, appAddress, secret)
Expand Down Expand Up @@ -207,7 +207,7 @@ func TestStoreToCache(t *testing.T) {
}
cfg.Cloud = cloud

appConn, err := grpc.NewAppConn(ctx, cloud.AppAddress, cloud.Secret, cloud.ID, logger)
appConn, err := grpc.NewAppConn(ctx, cloud.AppAddress, cloud.ID, cfg.Cloud.GetCloudCredsDialOpt(), logger)
test.That(t, err, test.ShouldBeNil)
defer appConn.Close()

Expand Down
4 changes: 2 additions & 2 deletions config/watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,8 @@ func TestNewWatcherCloud(t *testing.T) {

storeConfigInServer(confToReturn)

appConn, err := grpc.NewAppConn(context.Background(), confToReturn.Cloud.AppAddress, confToReturn.Cloud.Secret, confToReturn.Cloud.ID,
logger)
appConn, err := grpc.NewAppConn(
context.Background(), confToReturn.Cloud.AppAddress, confToReturn.Cloud.ID, confToReturn.Cloud.GetCloudCredsDialOpt(), logger)
test.That(t, err, test.ShouldBeNil)
defer appConn.Close()
watcher, err := config.NewWatcher(context.Background(), &config.Config{Cloud: newCloudConf()}, logger, appConn)
Expand Down
3 changes: 2 additions & 1 deletion examples/customresources/demos/remoteserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (er

var appConn rpc.ClientConn
if cfg.Cloud != nil && cfg.Cloud.AppAddress != "" {
appConn, err = grpc.NewAppConn(ctx, cfg.Cloud.AppAddress, cfg.Cloud.Secret, cfg.Cloud.ID, logger)
cloudCreds := cfg.Cloud.GetCloudCredsDialOpt()
appConn, err = grpc.NewAppConn(ctx, cfg.Cloud.AppAddress, cfg.Cloud.ID, cloudCreds, logger)
if err != nil {
return nil
}
Expand Down
24 changes: 7 additions & 17 deletions grpc/app_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,25 @@ type AppConn struct {
// establishing a connection to App will continue to occur, however, in a background Goroutine. These attempts will continue until a
// connection is made. If `cloud` is nil, an `AppConn` with a nil underlying connection will return, and the background dialer will not
// start.
func NewAppConn(ctx context.Context, appAddress, secret, id string, logger logging.Logger) (rpc.ClientConn, error) {
func NewAppConn(ctx context.Context, appAddress, partID string, cloudCreds rpc.DialOption, logger logging.Logger) (rpc.ClientConn, error) {
appConn := &AppConn{ReconfigurableClientConn: &ReconfigurableClientConn{Logger: logger.Sublogger("app_conn")}}

grpcURL, err := url.Parse(appAddress)
if err != nil {
return nil, err
}

dialOpts := dialOpts(secret, id)
dialOpts := make([]rpc.DialOption, 0, 2)

if cloudCreds != nil {
dialOpts = append(dialOpts, cloudCreds)
}

if grpcURL.Scheme == "http" {
dialOpts = append(dialOpts, rpc.WithInsecure())
}

ctxWithTimeout, ctxWithTimeoutCancel := contextutils.GetTimeoutCtx(ctx, true, id, logger)
ctxWithTimeout, ctxWithTimeoutCancel := contextutils.GetTimeoutCtx(ctx, true, partID, logger)
defer ctxWithTimeoutCancel()
// there will always be a deadline
if deadline, ok := ctxWithTimeout.Deadline(); ok {
Expand Down Expand Up @@ -131,17 +135,3 @@ func (ac *AppConn) Close() error {

return ac.ReconfigurableClientConn.Close()
}

func dialOpts(secret, id string) []rpc.DialOption {
dialOpts := make([]rpc.DialOption, 0, 2)
// Only add credentials when secret is set.
if secret != "" {
dialOpts = append(dialOpts, rpc.WithEntityCredentials(id,
rpc.Credentials{
Type: "robot-secret",
Payload: secret,
},
))
}
return dialOpts
}
Loading