diff --git a/README.md b/README.md index be2f7b21..937b1c62 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ Flags: --excluded-databases strings Database names to exclude from sync, case-insensitive. Can be specified multiple times. ($BATON_EXCLUDED_DATABASES) -f, --file string The path to the c1z file to sync with ($BATON_FILE) (default "sync.c1z") -h, --help help for baton-snowflake +--issue-credentials Enable issuing Snowflake programmatic access tokens for existing users. ($BATON_ISSUE_CREDENTIALS) --log-format string The output format for logs: json, console ($BATON_LOG_FORMAT) (default "json") --log-level string The log level: debug, info, warn, error ($BATON_LOG_LEVEL) (default "info") --private-key string Private Key (PEM format). ($BATON_PRIVATE_KEY) diff --git a/cmd/baton-snowflake/main.go b/cmd/baton-snowflake/main.go index 2ad8f32b..48973ec8 100644 --- a/cmd/baton-snowflake/main.go +++ b/cmd/baton-snowflake/main.go @@ -23,6 +23,6 @@ func main() { cfg.ConfigurationSchema(), connector.New, connectorrunner.WithSessionStoreEnabled(), - connectorrunner.WithDefaultCapabilitiesConnectorBuilderV2(&connector.Connector{SyncSecrets: true}), + connectorrunner.WithDefaultCapabilitiesConnectorBuilderV2(&connector.Connector{SyncSecrets: true, IssueCredentials: true}), ) } diff --git a/config_schema.json b/config_schema.json index 791950a7..637c3f47 100644 --- a/config_schema.json +++ b/config_schema.json @@ -145,6 +145,12 @@ "description": "Enable synchronization of Snowflake secrets. When enabled, the connector will sync secrets from your Snowflake account.", "boolField": {} }, + { + "name": "issue-credentials", + "displayName": "Issue Credentials", + "description": "Enable issuing Snowflake programmatic access tokens for existing users. Independent of Sync Secrets: this also syncs the tokens it issues so they can be revoked, but no other secrets.", + "boolField": {} + }, { "name": "excluded-databases", "displayName": "Excluded Databases", diff --git a/docs/connector.mdx b/docs/connector.mdx index b912e582..4cc77e5d 100644 --- a/docs/connector.mdx +++ b/docs/connector.mdx @@ -28,7 +28,9 @@ The Snowflake connector supports [account provisioning](/product/admin/account-p ### Issuing programmatic access tokens -The connector can issue a Snowflake [programmatic access token](https://docs.snowflake.com/en/user-guide/programmatic-access-tokens) for an existing user, and can revoke one it has issued. Issued tokens are synced back as **Programmatic access token** resources, so they appear in your inventory alongside the user they belong to. +Enable **Issue credentials** to let the connector issue a Snowflake [programmatic access token](https://docs.snowflake.com/en/user-guide/programmatic-access-tokens) for an existing user, and revoke one it has issued. Issued tokens are synced back as **Programmatic access token** resources, so they appear in your inventory alongside the user they belong to. + +**Issue credentials** and **Sync secrets** are independent. Enabling **Issue credentials** syncs the programmatic access tokens the connector can revoke, and nothing else; enabling **Sync secrets** inventories Snowflake secrets and RSA public keys without granting the ability to mint tokens. Enable both if you want a full secret inventory and issuance. Token lifetime is set by the requester. Snowflake accepts whole days only, and the connector rounds down so a token never outlives the requested expiry. The minimum is one day and the maximum is one year; when no expiry is requested the token lasts 15 days. @@ -44,7 +46,7 @@ Token lifetime is set by the requester. Snowflake accepts whole days only, and t **License data is opt-in and requires an organization account.** License resources report the Snowflake edition (Standard, Enterprise, or Business Critical) and, for single-account organizations, the number of users as consumed seats. Reading it requires connecting with an account that can view organization-level details, so enable this capability only when that access is available. -[This connector can sync secrets](/product/admin/inventory) and display them on the **Inventory** page. +[This connector can sync secrets](/product/admin/inventory) and display them on the **Inventory** page, and can issue programmatic access tokens. Both are opt-in and configured separately. ### Connector actions @@ -174,7 +176,10 @@ In the **Username** field, enter your Snowflake username. In the **RSA Private Key (PEM Format)** field, upload the private key file. -**Optional.** Enable **Sync secrets** to display them on the [Inventory page](/product/admin/inventory). +**Optional.** Enable **Sync secrets** to inventory Snowflake secrets and RSA public keys on the [Inventory page](/product/admin/inventory). + + +**Optional.** Enable **Issue credentials** to allow issuing and revoking programmatic access tokens. This is separate from **Sync secrets** — see [Issuing programmatic access tokens](#issuing-programmatic-access-tokens) for the prerequisites. **Optional.** In the **Excluded Databases** field, enter the names of any Snowflake databases you want to skip during sync. You can add multiple names. Matching is case-insensitive. Excluded databases and all their tables are omitted from every sync. diff --git a/pkg/config/conf.gen.go b/pkg/config/conf.gen.go index a8e30cbc..d84241ae 100644 --- a/pkg/config/conf.gen.go +++ b/pkg/config/conf.gen.go @@ -10,6 +10,7 @@ type Snowflake struct { PrivateKeyPath string `mapstructure:"private-key-path"` UserIdentifier string `mapstructure:"user-identifier"` SyncSecrets bool `mapstructure:"sync-secrets"` + IssueCredentials bool `mapstructure:"issue-credentials"` ExcludedDatabases []string `mapstructure:"excluded-databases"` } diff --git a/pkg/config/config.go b/pkg/config/config.go index 64de7d4c..7efa2661 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -45,6 +45,15 @@ var ( field.WithDescription("Enable synchronization of Snowflake secrets. When enabled, the connector will sync secrets from your Snowflake account."), field.WithDefaultValue(false), ) + IssueCredentials = field.BoolField( + "issue-credentials", + field.WithDisplayName("Issue Credentials"), + field.WithDescription( + "Enable issuing Snowflake programmatic access tokens for existing users. Independent of "+ + "Sync Secrets: this also syncs the tokens it issues so they can be revoked, but no other secrets.", + ), + field.WithDefaultValue(false), + ) ExcludedDatabases = field.StringSliceField( "excluded-databases", field.WithDisplayName("Excluded Databases"), @@ -69,6 +78,7 @@ var ( PrivateKeyPathField, UserIdentifierField, SyncSecrets, + IssueCredentials, ExcludedDatabases, } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index f743ff89..c52c0dda 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -20,14 +20,16 @@ import ( type Connector struct { Client *snowflake.Client SyncSecrets bool + IssueCredentials bool excludedDatabases []string } // ResourceSyncers returns a ResourceSyncerV2 for each resource type that should be synced from the upstream service. func (d *Connector) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncerV2 { - userSyncer := connectorbuilder.ResourceSyncerV2(newUserBuilder(d.Client, d.SyncSecrets)) - if d.SyncSecrets { - userSyncer = newCredentialUserBuilder(d.Client, d.SyncSecrets) + secrets := secretOptions{syncSecrets: d.SyncSecrets, issueCredentials: d.IssueCredentials} + userSyncer := connectorbuilder.ResourceSyncerV2(newUserBuilder(d.Client, secrets)) + if d.IssueCredentials { + userSyncer = newCredentialUserBuilder(d.Client, secrets) } builders := []connectorbuilder.ResourceSyncerV2{ userSyncer, @@ -39,12 +41,10 @@ func (d *Connector) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso } if d.SyncSecrets { - builders = append( - builders, - newSecretBuilder(d.Client), - newRsaBuilder(d.Client), - newProgrammaticAccessTokenBuilder(d.Client), - ) + builders = append(builders, newSecretBuilder(d.Client), newRsaBuilder(d.Client)) + } + if secrets.tokensSynced() { + builders = append(builders, newProgrammaticAccessTokenBuilder(d.Client)) } return builders @@ -264,6 +264,7 @@ func New(ctx context.Context, cfg *config.Snowflake, _ *cli.ConnectorOpts) (conn return &Connector{ Client: client, SyncSecrets: cfg.SyncSecrets, + IssueCredentials: cfg.IssueCredentials, excludedDatabases: cfg.ExcludedDatabases, }, nil, nil } diff --git a/pkg/connector/connector_test.go b/pkg/connector/connector_test.go index b34b83ec..c654be7b 100644 --- a/pkg/connector/connector_test.go +++ b/pkg/connector/connector_test.go @@ -1,9 +1,14 @@ package connector import ( + "context" + "sort" "testing" + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-snowflake/pkg/snowflake" + "github.com/stretchr/testify/require" ) func TestMissingLoginPrivilegeErr(t *testing.T) { @@ -35,3 +40,96 @@ func TestMissingLoginPrivilegeErr(t *testing.T) { }) } } + +// sync-secrets and issue-credentials are independent, but not unrelated: issuance +// advertises DISCOVERABLE, so turning it on has to make the token type syncable even +// when the broader secret sync is off. Otherwise an issued credential exists with +// nothing holding a handle to revoke it. +func TestSecretFlagsGateIndependently(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + syncSecrets bool + issueCredentials bool + wantIssuer bool + wantTypes []string + }{ + { + name: "neither", + wantTypes: []string{"account_role", "database", "integration", "license", "table", "user"}, + }, + { + name: "inventory without minting", + syncSecrets: true, + wantTypes: []string{ + "account_role", "database", "integration", "license", + "programmatic_access_token", "rsa_public_key", "secret", "table", "user", + }, + }, + { + name: "minting without a full inventory", + issueCredentials: true, + wantIssuer: true, + wantTypes: []string{ + "account_role", "database", "integration", "license", + "programmatic_access_token", "table", "user", + }, + }, + { + name: "both", + syncSecrets: true, + issueCredentials: true, + wantIssuer: true, + wantTypes: []string{ + "account_role", "database", "integration", "license", + "programmatic_access_token", "rsa_public_key", "secret", "table", "user", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + server, err := connectorbuilder.NewConnector(context.Background(), &Connector{ + SyncSecrets: tc.syncSecrets, IssueCredentials: tc.issueCredentials, + }) + require.NoError(t, err) + response, err := server.GetMetadata(context.Background(), &v2.ConnectorServiceGetMetadataRequest{}) + require.NoError(t, err) + + gotTypes, gotIssuer := []string{}, false + for _, capability := range response.GetMetadata().GetCapabilities().GetResourceTypeCapabilities() { + gotTypes = append(gotTypes, capability.GetResourceType().GetId()) + if capability.GetResourceType().GetId() == userResourceType.Id && capability.GetCredentialIssue() != nil { + gotIssuer = true + } + } + sort.Strings(gotTypes) + require.Equal(t, tc.wantTypes, gotTypes) + require.Equal(t, tc.wantIssuer, gotIssuer, "credential issuance advertised") + + // The child annotations have to move with the resource types, or a synced + // type is registered but never walked per user. + resource, err := userResource(context.Background(), + &snowflake.User{Username: "service-user", Type: "SERVICE"}, + secretOptions{syncSecrets: tc.syncSecrets, issueCredentials: tc.issueCredentials}) + require.NoError(t, err) + children := []string{} + for _, annotation := range resource.GetAnnotations() { + child := &v2.ChildResourceType{} + if annotation.MessageIs(child) { + require.NoError(t, annotation.UnmarshalTo(child)) + children = append(children, child.GetResourceTypeId()) + } + } + sort.Strings(children) + want := []string{} + if tc.syncSecrets { + want = append(want, rsaPublicKeyResourceType.Id) + } + if tc.syncSecrets || tc.issueCredentials { + want = append(want, programmaticAccessTokenResourceType.Id) + } + sort.Strings(want) + require.Equal(t, want, children) + }) + } +} diff --git a/pkg/connector/programmatic_access_tokens_test.go b/pkg/connector/programmatic_access_tokens_test.go index d218097d..b83c41c8 100644 --- a/pkg/connector/programmatic_access_tokens_test.go +++ b/pkg/connector/programmatic_access_tokens_test.go @@ -28,7 +28,7 @@ func TestCredentialUserBuilderIssueServiceUserUsesDefaultRoleRestriction(t *test t.Fatalf("new client: %v", err) } - _, err = newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + _, err = newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", }) @@ -49,7 +49,7 @@ func TestCredentialUserBuilderIssueServiceUserWithUnassignedDefaultRoleFailsBefo t.Fatalf("new client: %v", err) } - _, err = newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + _, err = newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", }) @@ -212,7 +212,7 @@ func TestProgrammaticAccessTokenIDRoundTrip(t *testing.T) { } func TestCredentialIssuanceCapabilitiesRegisterWithDeleter(t *testing.T) { - server, err := connectorbuilder.NewConnector(context.Background(), &Connector{SyncSecrets: true}) + server, err := connectorbuilder.NewConnector(context.Background(), &Connector{IssueCredentials: true}) if err != nil { t.Fatalf("NewConnector() error = %v", err) } @@ -242,7 +242,7 @@ func TestCredentialIssuanceCapabilitiesRegisterWithDeleter(t *testing.T) { } func TestIssueCapabilityDetails(t *testing.T) { - details, _, err := newCredentialUserBuilder(nil, true).IssueCapabilityDetails(context.Background()) + details, _, err := newCredentialUserBuilder(nil, secretOptions{issueCredentials: true}).IssueCapabilityDetails(context.Background()) if err != nil { t.Fatalf("IssueCapabilityDetails() error = %v", err) } @@ -268,7 +268,7 @@ func TestCredentialUserBuilderIssueServiceUserWithNullDefaultRoleReportsMissingR t.Fatalf("new client: %v", err) } - _, err = newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + _, err = newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", }) @@ -292,7 +292,7 @@ func TestCredentialUserBuilderIssueRemovesTokenWhenProviderDoesNotReturnIt(t *te t.Fatalf("new client: %v", err) } - _, err = newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + _, err = newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", }) @@ -310,28 +310,26 @@ func TestCredentialUserBuilderIssueRemovesTokenWhenProviderDoesNotReturnIt(t *te func TestUserResourceAdvertisesTokenAsChildResourceType(t *testing.T) { // The syncer walks a child type per parent only when the parent carries this // annotation. Without it an issued token is never discovered by a sync, which - // contradicts the DISCOVERABLE mode the issuer advertises. - resource, err := userResource(context.Background(), &snowflake.User{Username: "service-user", Type: "SERVICE"}, true) + // contradicts the DISCOVERABLE mode the issuer advertises. issue-credentials alone + // must be enough: it is the flag that makes tokens exist in the first place. + resource, err := userResource(context.Background(), &snowflake.User{Username: "service-user", Type: "SERVICE"}, secretOptions{issueCredentials: true}) if err != nil { t.Fatalf("userResource() error = %v", err) } - want := map[string]bool{ - rsaPublicKeyResourceType.Id: false, - programmaticAccessTokenResourceType.Id: false, - } + found := false for _, annotation := range resource.GetAnnotations() { child := &v2.ChildResourceType{} if annotation.MessageIs(child) { if err := annotation.UnmarshalTo(child); err != nil { t.Fatalf("unmarshal child resource type: %v", err) } - want[child.GetResourceTypeId()] = true + if child.GetResourceTypeId() == programmaticAccessTokenResourceType.Id { + found = true + } } } - for id, found := range want { - if !found { - t.Fatalf("user resource is missing ChildResourceType %q", id) - } + if !found { + t.Fatalf("user resource is missing ChildResourceType %q", programmaticAccessTokenResourceType.Id) } } @@ -352,7 +350,7 @@ func TestCredentialUserBuilderIssueKeepsTokenWhenReadBackIsDenied(t *testing.T) t.Fatalf("new client: %v", err) } - output, err := newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + output, err := newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", }) @@ -391,7 +389,7 @@ func TestCredentialUserBuilderIssueProceedsWhenRoleCheckIsDenied(t *testing.T) { t.Fatalf("new client: %v", err) } - _, err = newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + _, err = newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", }) @@ -422,7 +420,7 @@ func TestCredentialUserBuilderIssueSamplesExpiryAfterPreflight(t *testing.T) { } requested := time.Now().UTC().Add(2*24*time.Hour + 50*time.Millisecond) - _, err = newCredentialUserBuilder(client, true).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ + _, err = newCredentialUserBuilder(client, secretOptions{issueCredentials: true}).Issue(context.Background(), &connectorbuilder.CredentialIssueInput{ IdentityID: v2.ResourceId_builder{ResourceType: userResourceType.Id, Resource: "service-user"}.Build(), RequestID: "request-1", ExpiresAt: timestamppb.New(requested), diff --git a/pkg/connector/tables.go b/pkg/connector/tables.go index 99712f60..31e29618 100644 --- a/pkg/connector/tables.go +++ b/pkg/connector/tables.go @@ -464,7 +464,7 @@ func (o *tableBuilder) Grants(ctx context.Context, resource *v2.Resource, opts r if user == nil { continue } - principalResource, err = userResource(ctx, user, false) + principalResource, err = userResource(ctx, user, secretOptions{}) if err != nil { return nil, nil, wrapError(err, fmt.Sprintf("failed to build resource for user %q", tg.GranteeName)) } diff --git a/pkg/connector/tables_test.go b/pkg/connector/tables_test.go index b2530119..d57f60b7 100644 --- a/pkg/connector/tables_test.go +++ b/pkg/connector/tables_test.go @@ -879,7 +879,6 @@ func newDatabaseGrantsStatusMockServer(t *testing.T, owner string, rolesStatus i })) } - // TestTableBuilder_List_EnumeratesSchemasWhenVisible is the control for the test above: it // proves the mock drives the real code path, so the 422 case cannot pass for the wrong reason. func TestTableBuilder_List_EnumeratesSchemasWhenVisible(t *testing.T) { diff --git a/pkg/connector/users.go b/pkg/connector/users.go index 83de0e1d..f172c827 100644 --- a/pkg/connector/users.go +++ b/pkg/connector/users.go @@ -24,12 +24,29 @@ import ( type userBuilder struct { resourceType *v2.ResourceType client *snowflake.Client - syncSecrets bool + secrets secretOptions } -// credentialUserBuilder opts into credential issuance only when secret syncing -// is enabled. The issued-token resource type is therefore registered alongside -// the issuer, which is required by the SDK's build-time revoke validation. +// secretOptions carries the two independent gates for secret-bearing behaviour: +// sync-secrets lists secrets that already exist, and issue-credentials mints +// programmatic access tokens. A tenant may want inventory without minting, or +// minting without a full secret inventory, so neither implies the other. +type secretOptions struct { + syncSecrets bool + issueCredentials bool +} + +// tokensSynced reports whether the programmatic access token type is synced. +// Issuance advertises DISCOVERABLE, so turning it on has to make issued tokens +// syncable even when the broader secret sync is off; otherwise the credential +// exists with nothing holding a handle to revoke it. +func (s secretOptions) tokensSynced() bool { + return s.syncSecrets || s.issueCredentials +} + +// credentialUserBuilder adds credential issuance to the user syncer, and is +// registered only when issue-credentials is set. The issued-token resource type is +// registered alongside it, which the SDK's build-time revoke validation requires. type credentialUserBuilder struct { *userBuilder } @@ -42,8 +59,8 @@ const ( programmaticAccessTokenDefaultDays = 15 ) -func newCredentialUserBuilder(client *snowflake.Client, syncSecrets bool) *credentialUserBuilder { - return &credentialUserBuilder{userBuilder: newUserBuilder(client, syncSecrets)} +func newCredentialUserBuilder(client *snowflake.Client, secrets secretOptions) *credentialUserBuilder { + return &credentialUserBuilder{userBuilder: newUserBuilder(client, secrets)} } func (o *credentialUserBuilder) IssueCapabilityDetails(_ context.Context) (*v2.CredentialDetailsCredentialIssue, annotations.Annotations, error) { @@ -202,7 +219,7 @@ func (o *userBuilder) ResourceType(ctx context.Context) *v2.ResourceType { return userResourceType } -func userResource(_ context.Context, user *snowflake.User, syncSecrets bool) (*v2.Resource, error) { +func userResource(_ context.Context, user *snowflake.User, secrets secretOptions) (*v2.Resource, error) { profile := map[string]interface{}{ "email": user.Email, "login": user.Login, @@ -238,15 +255,15 @@ func userResource(_ context.Context, user *snowflake.User, syncSecrets bool) (*v rs.WithResourceProfile(profile), rs.WithResourceStatus(getUserStatus(user), getUserDetailedStatus(user)), } - if syncSecrets { + if secrets.syncSecrets { + opts = append(opts, rs.WithAnnotation(&v2.ChildResourceType{ResourceTypeId: rsaPublicKeyResourceType.Id})) + } + if secrets.tokensSynced() { // The syncer only calls a child type's List with a parent when the parent - // carries this annotation. Without the token entry an issued programmatic - // access token is never discovered by a sync, which contradicts the - // DISCOVERABLE resource mode the issuer advertises. - opts = append(opts, - rs.WithAnnotation(&v2.ChildResourceType{ResourceTypeId: rsaPublicKeyResourceType.Id}), - rs.WithAnnotation(&v2.ChildResourceType{ResourceTypeId: programmaticAccessTokenResourceType.Id}), - ) + // carries this annotation. Without it an issued programmatic access token is + // never discovered by a sync, which contradicts the DISCOVERABLE resource + // mode the issuer advertises. + opts = append(opts, rs.WithAnnotation(&v2.ChildResourceType{ResourceTypeId: programmaticAccessTokenResourceType.Id})) } if nhiType, nhiDetail, isNHI := classifyUserNHI(user.Type); isNHI { opts = append(opts, rs.WithNHIType(nhiType, nhiDetail)) @@ -389,7 +406,7 @@ func (o *userBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId, var resources []*v2.Resource for _, user := range users { - resource, err := userResource(ctx, &user, o.syncSecrets) // #nosec G601 + resource, err := userResource(ctx, &user, o.secrets) // #nosec G601 if err != nil { return nil, nil, wrapError(err, "failed to create user resource") } @@ -511,7 +528,7 @@ func (o *userBuilder) CreateAccount( } // Build resource for the new user - resource, err := userResource(ctx, user, o.syncSecrets) + resource, err := userResource(ctx, user, o.secrets) if err != nil { return nil, nil, nil, wrapError(err, "failed to create user resource") } @@ -629,10 +646,10 @@ func (o *userBuilder) Delete(ctx context.Context, resourceId *v2.ResourceId, par return nil, nil } -func newUserBuilder(client *snowflake.Client, syncSecrets bool) *userBuilder { +func newUserBuilder(client *snowflake.Client, secrets secretOptions) *userBuilder { return &userBuilder{ resourceType: userResourceType, client: client, - syncSecrets: syncSecrets, + secrets: secrets, } } diff --git a/pkg/connector/users_test.go b/pkg/connector/users_test.go index b8568c9e..fca4fc1c 100644 --- a/pkg/connector/users_test.go +++ b/pkg/connector/users_test.go @@ -110,7 +110,7 @@ func TestUserResourceNHIAnnotation(t *testing.T) { ctx := context.Background() serviceUser := &snowflake.User{Username: "svc", Type: "SERVICE"} - res, err := userResource(ctx, serviceUser, false) + res, err := userResource(ctx, serviceUser, secretOptions{}) if err != nil { t.Fatalf("userResource() error = %v", err) } @@ -126,7 +126,7 @@ func TestUserResourceNHIAnnotation(t *testing.T) { } personUser := &snowflake.User{Username: "alice", Type: "PERSON"} - res, err = userResource(ctx, personUser, false) + res, err = userResource(ctx, personUser, secretOptions{}) if err != nil { t.Fatalf("userResource() error = %v", err) }