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)
}