Skip to content
Closed
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
58 changes: 55 additions & 3 deletions baton_capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@
"permissions": {},
"optInRequired": true
},
{
"resourceType": {
"id": "named_key_pair",
"displayName": "Named Key Pair",
"traits": [
"TRAIT_SECRET"
],
"annotations": [
{
"@type": "type.googleapis.com/c1.connector.v2.SkipEntitlementsAndGrants"
},
{
"@type": "type.googleapis.com/c1.connector.v2.OptInRequired"
}
]
},
"capabilities": [
"CAPABILITY_SYNC",
"CAPABILITY_RESOURCE_DELETE"
],
"permissions": {},
"optInRequired": true
},
{
"resourceType": {
"id": "rsa_public_key",
Expand Down Expand Up @@ -133,16 +156,45 @@
"capabilities": [
"CAPABILITY_SYNC",
"CAPABILITY_ACCOUNT_PROVISIONING",
"CAPABILITY_RESOURCE_DELETE"
"CAPABILITY_RESOURCE_DELETE",
"CAPABILITY_CREDENTIAL_ISSUE"
],
"permissions": {}
"permissions": {},
"credentialIssue": {
"options": [
{
"option": "CAPABILITY_DETAIL_CREDENTIAL_OPTION_KEYPAIR",
"keyProfiles": [
{
"kty": "RSA",
"rsaModulusBits": 2048
},
{
"kty": "RSA",
"rsaModulusBits": 3072
},
{
"kty": "RSA",
"rsaModulusBits": 4096
}
],
"expiry": {
"min": "86400s"
},
"resourceMode": "CREDENTIAL_RESOURCE_MODE_DISCOVERABLE",
"secretResourceTypeId": "named_key_pair"
}
],
"preferredOption": "CAPABILITY_DETAIL_CREDENTIAL_OPTION_KEYPAIR"
}
}
],
"connectorCapabilities": [
"CAPABILITY_PROVISION",
"CAPABILITY_SYNC",
"CAPABILITY_ACCOUNT_PROVISIONING",
"CAPABILITY_RESOURCE_DELETE"
"CAPABILITY_RESOURCE_DELETE",
"CAPABILITY_CREDENTIAL_ISSUE"
],
"credentialDetails": {
"capabilityAccountProvisioning": {
Expand Down
6 changes: 6 additions & 0 deletions docs/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ The Snowflake connector supports [account provisioning](/product/admin/account-p

[This connector can sync secrets](/product/admin/inventory) and display them on the **Inventory** page.

When **Sync secrets** is enabled, the connector also inventories Snowflake
named key pairs and can issue a new named RSA key pair for `SERVICE` and
`LEGACY_SERVICE` users. C1 encrypts the one-time private key at the connector
boundary. Supported RSA modulus sizes are 2048, 3072, and 4096 bits. An optional
credential lifetime must be a positive whole number of days.

## Gather Snowflake credentials

Configuring the connector requires you to pass in credentials generated in Snowflake. Gather these credentials before you move on.
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ require (
google.golang.org/grpc v1.81.0
)

require golang.org/x/oauth2 v0.36.0
require (
github.com/segmentio/ksuid v1.0.4
golang.org/x/oauth2 v0.36.0
google.golang.org/protobuf v1.36.11
)

require (
filippo.io/age v1.3.1 // indirect
Expand Down Expand Up @@ -101,7 +105,6 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shirou/gopsutil/v4 v4.26.4 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
Expand Down Expand Up @@ -137,7 +140,6 @@ require (
golang.org/x/text v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
1 change: 1 addition & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (d *Connector) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso
builders,
newSecretBuilder(d.Client),
newRsaBuilder(d.Client),
newNamedKeyPairBuilder(d.Client),
)
}

Expand Down
67 changes: 67 additions & 0 deletions pkg/connector/connector_capabilities_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package connector

import (
"context"
"slices"
"testing"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
"github.com/conductorone/baton-snowflake/pkg/snowflake"
"github.com/stretchr/testify/require"
)

type capabilityProvider interface {
GetCapabilities(context.Context) (*v2.ConnectorCapabilities, error)
}

func connectorCapabilities(t *testing.T, syncSecrets bool) map[string]*v2.ResourceTypeCapability {
t.Helper()
server, err := connectorbuilder.NewConnector(context.Background(), &Connector{
Client: &snowflake.Client{},
SyncSecrets: syncSecrets,
})
require.NoError(t, err)
provider, ok := server.(capabilityProvider)
require.True(t, ok)
capabilities, err := provider.GetCapabilities(context.Background())
require.NoError(t, err)

byID := make(map[string]*v2.ResourceTypeCapability, len(capabilities.GetResourceTypeCapabilities()))
for _, capability := range capabilities.GetResourceTypeCapabilities() {
byID[capability.GetResourceType().GetId()] = capability
}
return byID
}

func TestNamedKeyPairCapabilitiesAreOptInAndLifecycleComplete(t *testing.T) {
capabilities := connectorCapabilities(t, true)
namedKeyPair, ok := capabilities[namedKeyPairResourceType.Id]
require.True(t, ok)
require.True(t, namedKeyPair.GetOptInRequired())
require.True(t, slices.Contains(namedKeyPair.GetCapabilities(), v2.Capability_CAPABILITY_SYNC))
require.True(t, slices.Contains(namedKeyPair.GetCapabilities(), v2.Capability_CAPABILITY_RESOURCE_DELETE))
require.False(t, slices.Contains(namedKeyPair.GetCapabilities(), v2.Capability_CAPABILITY_CREDENTIAL_ISSUE))
annos := annotations.Annotations(namedKeyPair.GetResourceType().GetAnnotations())
require.True(t, annos.Contains(&v2.SkipEntitlementsAndGrants{}))

user := capabilities[userResourceType.Id]
require.NotNil(t, user)
require.True(t, slices.Contains(user.GetCapabilities(), v2.Capability_CAPABILITY_CREDENTIAL_ISSUE))
require.NotNil(t, user.GetCredentialIssue())
require.Len(t, user.GetCredentialIssue().GetOptions(), 1)
require.Equal(t, namedKeyPairResourceType.Id, user.GetCredentialIssue().GetOptions()[0].GetSecretResourceTypeId())
require.Equal(t, v2.CredentialResourceMode_CREDENTIAL_RESOURCE_MODE_DISCOVERABLE, user.GetCredentialIssue().GetOptions()[0].GetResourceMode())
}

func TestNamedKeyPairCapabilitiesAreAbsentWithoutSecretSync(t *testing.T) {
capabilities := connectorCapabilities(t, false)
_, ok := capabilities[namedKeyPairResourceType.Id]
require.False(t, ok)

user := capabilities[userResourceType.Id]
require.NotNil(t, user)
require.False(t, slices.Contains(user.GetCapabilities(), v2.Capability_CAPABILITY_CREDENTIAL_ISSUE))
require.Nil(t, user.GetCredentialIssue())
}
Loading
Loading