From d79e1e070c86ff1a221937f4469e0302bff78e8e Mon Sep 17 00:00:00 2001 From: Geoff Greer Date: Mon, 8 Jan 2024 16:53:43 -0800 Subject: [PATCH] Add support for rotating credentials. --- pkg/cli/cli.go | 25 ++++++++- pkg/connectorrunner/runner.go | 48 ++++++++++++----- pkg/provisioner/provisioner.go | 98 +++++++++++++++++++++++++++------- pkg/tasks/local/rotator.go | 55 +++++++++++++++++++ 4 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 pkg/tasks/local/rotator.go diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index fee0e8d1..45cb2fcb 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -124,6 +124,14 @@ func NewCmd[T any, PtrT *T]( v.GetString("delete-resource"), v.GetString("delete-resource-type"), )) + case v.GetString("rotate-credentials") != "": + opts = append(opts, + connectorrunner.WithProvisioningEnabled(), + connectorrunner.WithOnDemandRotateCredentials( + v.GetString("file"), + v.GetString("rotate-credentials"), + v.GetString("rotate-credentials-type"), + )) default: opts = append(opts, connectorrunner.WithOnDemandSync(v.GetString("file"))) } @@ -195,6 +203,8 @@ func NewCmd[T any, PtrT *T]( copts = append(copts, connector.WithProvisioningEnabled()) case v.GetString("delete-resource") != "" || v.GetString("delete-resource-type") != "": copts = append(copts, connector.WithProvisioningEnabled()) + case v.GetString("rotate-credentials") != "" || v.GetString("rotate-credentials-type") != "": + copts = append(copts, connector.WithProvisioningEnabled()) case v.GetBool("provisioning"): copts = append(copts, connector.WithProvisioningEnabled()) } @@ -317,8 +327,11 @@ func NewCmd[T any, PtrT *T]( cmd.PersistentFlags().String("delete-resource", "", "The id of the resource to delete ($BATON_DELETE_RESOURCE)") cmd.PersistentFlags().String("delete-resource-type", "", "The type of the resource to delete ($BATON_DELETE_RESOURCE_TYPE)") - cmd.MarkFlagsMutuallyExclusive("grant-entitlement", "revoke-grant", "create-account-login", "delete-resource") - cmd.MarkFlagsMutuallyExclusive("grant-entitlement", "revoke-grant", "create-account-email", "delete-resource-type") + cmd.PersistentFlags().String("rotate-credentials", "", "The id of the resource to rotate credentials on ($BATON_ROTATE_CREDENTIALS)") + cmd.PersistentFlags().String("rotate-credentials-type", "", "The type of the resource to rotate credentials on ($BATON_ROTATE_CREDENTIALS_TYPE)") + + cmd.MarkFlagsMutuallyExclusive("grant-entitlement", "revoke-grant", "create-account-login", "delete-resource", "rotate-credentials") + cmd.MarkFlagsMutuallyExclusive("grant-entitlement", "revoke-grant", "create-account-email", "delete-resource-type", "rotate-credentials-type") err = cmd.PersistentFlags().MarkHidden("grant-entitlement") if err != nil { return nil, err @@ -351,6 +364,14 @@ func NewCmd[T any, PtrT *T]( if err != nil { return nil, err } + err = cmd.PersistentFlags().MarkHidden("rotate-credentials") + if err != nil { + return nil, err + } + err = cmd.PersistentFlags().MarkHidden("rotate-credentials-type") + if err != nil { + return nil, err + } // Flags for daemon mode cmd.PersistentFlags().String("client-id", "", "The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)") diff --git a/pkg/connectorrunner/runner.go b/pkg/connectorrunner/runner.go index 7adcf566..5e96d4f6 100644 --- a/pkg/connectorrunner/runner.go +++ b/pkg/connectorrunner/runner.go @@ -211,20 +211,26 @@ type deleteResourceConfig struct { resourceType string } +type rotateCredentialsConfig struct { + resourceId string + resourceType string +} + type runnerConfig struct { - rlCfg *ratelimitV1.RateLimiterConfig - rlDescriptors []*ratelimitV1.RateLimitDescriptors_Entry - onDemand bool - c1zPath string - clientAuth bool - clientID string - clientSecret string - provisioningEnabled bool - grantConfig *grantConfig - revokeConfig *revokeConfig - tempDir string - createAccountConfig *createAccountConfig - deleteResourceConfig *deleteResourceConfig + rlCfg *ratelimitV1.RateLimiterConfig + rlDescriptors []*ratelimitV1.RateLimitDescriptors_Entry + onDemand bool + c1zPath string + clientAuth bool + clientID string + clientSecret string + provisioningEnabled bool + grantConfig *grantConfig + revokeConfig *revokeConfig + tempDir string + createAccountConfig *createAccountConfig + deleteResourceConfig *deleteResourceConfig + rotateCredentialsConfig *rotateCredentialsConfig } // WithRateLimiterConfig sets the RateLimiterConfig for a runner. @@ -357,6 +363,19 @@ func WithOnDemandDeleteResource(c1zPath string, resourceId string, resourceType return nil } } + +func WithOnDemandRotateCredentials(c1zPath string, resourceId string, resourceType string) Option { + return func(ctx context.Context, cfg *runnerConfig) error { + cfg.onDemand = true + cfg.c1zPath = c1zPath + cfg.rotateCredentialsConfig = &rotateCredentialsConfig{ + resourceId: resourceId, + resourceType: resourceType, + } + return nil + } +} + func WithOnDemandSync(c1zPath string) Option { return func(ctx context.Context, cfg *runnerConfig) error { cfg.onDemand = true @@ -434,6 +453,9 @@ func NewConnectorRunner(ctx context.Context, c types.ConnectorServer, opts ...Op case cfg.deleteResourceConfig != nil: tm = local.NewResourceDeleter(ctx, cfg.c1zPath, cfg.deleteResourceConfig.resourceId, cfg.deleteResourceConfig.resourceType) + case cfg.rotateCredentialsConfig != nil: + tm = local.NewCredentialRotator(ctx, cfg.c1zPath, cfg.rotateCredentialsConfig.resourceId, cfg.rotateCredentialsConfig.resourceType) + default: tm, err = local.NewSyncer(ctx, cfg.c1zPath, local.WithTmpDir(cfg.tempDir)) if err != nil { diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index eceee533..dde9ff5d 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -2,6 +2,7 @@ package provisioner import ( "context" + "crypto/ecdsa" "errors" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" @@ -33,6 +34,37 @@ type Provisioner struct { deleteResourceID string deleteResourceType string + + rotateCredentialsId string + rotateCredentialsType string +} + +// makeCrypto is used by rotateCredentials and createAccount. +func makeCrypto(ctx context.Context) (*ecdsa.PrivateKey, *v2.CredentialOptions, []*v2.EncryptionConfig, error) { + // Default to generating a random key and random password that is 12 characters long + privKey, pubKey := crypto.GenKey() + pubKeyJWKBytes, err := pubKey.MarshalJSON() + if err != nil { + return nil, nil, nil, err + } + opts := &v2.CredentialOptions{ + Create: true, + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 12, + }, + }, + } + config := []*v2.EncryptionConfig{ + { + Config: &v2.EncryptionConfig_PublicKeyConfig_{ + PublicKeyConfig: &v2.EncryptionConfig_PublicKeyConfig{ + PubKey: pubKeyJWKBytes, + }, + }, + }, + } + return privKey, opts, config, nil } func (p *Provisioner) Run(ctx context.Context) error { @@ -45,6 +77,8 @@ func (p *Provisioner) Run(ctx context.Context) error { return p.createAccount(ctx) case p.deleteResourceID != "" && p.deleteResourceType != "": return p.deleteResource(ctx) + case p.rotateCredentialsId != "" && p.rotateCredentialsType != "": + return p.rotateCredentials(ctx) default: return errors.New("unknown provisioning action") } @@ -183,29 +217,10 @@ func (p *Provisioner) createAccount(ctx context.Context) error { }) } - // Default to generating a random key and random password that is 12 characters long - privKey, pubKey := crypto.GenKey() - pubKeyJWKBytes, err := pubKey.MarshalJSON() + privKey, opts, config, err := makeCrypto(ctx) if err != nil { return err } - opts := &v2.CredentialOptions{ - Create: true, - Options: &v2.CredentialOptions_RandomPassword_{ - RandomPassword: &v2.CredentialOptions_RandomPassword{ - Length: 12, - }, - }, - } - config := []*v2.EncryptionConfig{ - { - Config: &v2.EncryptionConfig_PublicKeyConfig_{ - PublicKeyConfig: &v2.EncryptionConfig_PublicKeyConfig{ - PubKey: pubKeyJWKBytes, - }, - }, - }, - } result, err := p.connector.CreateAccount(ctx, &v2.CreateAccountRequest{ AccountInfo: &v2.AccountInfo{ @@ -246,6 +261,40 @@ func (p *Provisioner) deleteResource(ctx context.Context) error { return nil } +func (p *Provisioner) rotateCredentials(ctx context.Context) error { + l := ctxzap.Extract(ctx) + + privKey, opts, config, err := makeCrypto(ctx) + if err != nil { + return err + } + + result, err := p.connector.RotateCredential(ctx, &v2.RotateCredentialRequest{ + ResourceId: &v2.ResourceId{ + Resource: p.rotateCredentialsId, + ResourceType: p.rotateCredentialsType, + }, + CredentialOptions: opts, + EncryptionConfigs: config, + }) + if err != nil { + return err + } + + jwe, err := jose.ParseEncrypted(string(result.EncryptedData[0].EncryptedBytes)) + if err != nil { + return err + } + plaintext, err := jwe.Decrypt(privKey) + if err != nil { + return err + } + // TODO FIXME: do better + l.Info("credentials rotated", zap.String("resource", p.rotateCredentialsId), zap.String("resource type", p.rotateCredentialsType), zap.String("password", string(plaintext))) + + return nil +} + func NewGranter(c types.ConnectorClient, dbPath string, entitlementID string, principalID string, principalType string) *Provisioner { return &Provisioner{ dbPath: dbPath, @@ -281,3 +330,12 @@ func NewCreateAccountManager(c types.ConnectorClient, dbPath string, login strin createAccountEmail: email, } } + +func NewCredentialRotator(c types.ConnectorClient, dbPath string, resourceId string, resourceType string) *Provisioner { + return &Provisioner{ + dbPath: dbPath, + connector: c, + rotateCredentialsId: resourceId, + rotateCredentialsType: resourceType, + } +} diff --git a/pkg/tasks/local/rotator.go b/pkg/tasks/local/rotator.go new file mode 100644 index 00000000..81554a25 --- /dev/null +++ b/pkg/tasks/local/rotator.go @@ -0,0 +1,55 @@ +package local + +import ( + "context" + "sync" + "time" + + v1 "github.com/conductorone/baton-sdk/pb/c1/connectorapi/baton/v1" + "github.com/conductorone/baton-sdk/pkg/provisioner" + "github.com/conductorone/baton-sdk/pkg/tasks" + "github.com/conductorone/baton-sdk/pkg/types" +) + +type localCredentialRotator struct { + dbPath string + o sync.Once + + resourceId string + resourceType string +} + +func (m *localCredentialRotator) Next(ctx context.Context) (*v1.Task, time.Duration, error) { + var task *v1.Task + m.o.Do(func() { + task = &v1.Task{ + TaskType: &v1.Task_CreateAccount{}, + } + }) + return task, 0, nil +} + +func (m *localCredentialRotator) Process(ctx context.Context, task *v1.Task, cc types.ConnectorClient) error { + accountManager := provisioner.NewCredentialRotator(cc, m.dbPath, m.resourceId, m.resourceType) + + err := accountManager.Run(ctx) + if err != nil { + return err + } + + err = accountManager.Close(ctx) + if err != nil { + return err + } + + return nil +} + +// NewGranter returns a task manager that queues a sync task. +func NewCredentialRotator(ctx context.Context, dbPath string, resourceId string, resourceType string) tasks.Manager { + return &localCredentialRotator{ + dbPath: dbPath, + resourceId: resourceId, + resourceType: resourceType, + } +}