From 7109cae288a0a8ad5941e4e8bd6a4cf61bba9590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Gonz=C3=A1lez?= Date: Thu, 10 Sep 2026 16:28:57 -0700 Subject: [PATCH 1/4] Move to config.RunConnector and V2 resource syncers RunConnector is the SDK's current entrypoint and owns config decoding, the capabilities subcommand, and process exit codes (gRPC status codes), but it only accepts a ConnectorBuilderV2. All ten syncers therefore move from the V1 List/Entitlements/Grants contract to the V2 one: the page token arrives in SyncOpAttrs and the next token rides back in SyncOpResults. The translation is mechanical and no syncer's logic changes; three nil-token guards that can no longer trigger are dropped. Provisioning methods are unchanged, since the SDK registers them by type assertion regardless of builder version. The default-capabilities option stays so `capabilities` keeps working without a DSN. Co-Authored-By: Claude Fable 5.1 --- cmd/baton-postgresql/main.go | 36 ++++------------ pkg/connector/column.go | 44 ++++++++++---------- pkg/connector/connector.go | 4 +- pkg/connector/database.go | 78 ++++++++++++++++++----------------- pkg/connector/function.go | 50 +++++++++++----------- pkg/connector/large_object.go | 38 +++++++++-------- pkg/connector/procedure.go | 46 +++++++++++---------- pkg/connector/role.go | 31 +++++++------- pkg/connector/schema.go | 46 +++++++++++---------- pkg/connector/sequence.go | 52 ++++++++++++----------- pkg/connector/table.go | 56 +++++++++++++------------ pkg/connector/view.go | 50 +++++++++++----------- scripts/get-baton.sh | 43 ------------------- 13 files changed, 265 insertions(+), 309 deletions(-) delete mode 100755 scripts/get-baton.sh diff --git a/cmd/baton-postgresql/main.go b/cmd/baton-postgresql/main.go index b0c6acec..4c5c24f8 100644 --- a/cmd/baton-postgresql/main.go +++ b/cmd/baton-postgresql/main.go @@ -2,13 +2,11 @@ package main import ( "context" - "fmt" - "os" cfg "github.com/conductorone/baton-postgresql/pkg/config" + "github.com/conductorone/baton-sdk/pkg/cli" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/connectorrunner" - "github.com/conductorone/baton-sdk/pkg/types" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" @@ -21,38 +19,20 @@ var version = "dev" func main() { ctx := context.Background() - // The capabilities subcommand runs without a DSN: it builds the zero-value - // connector instead of validating the required flag. - _, cmd, err := configschema.DefineConfiguration(ctx, "baton-postgresql", getConnector, cfg.Config, - connectorrunner.WithDefaultCapabilitiesConnectorBuilder(&connector.Postgresql{})) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) - } - - cmd.Version = version - - err = cmd.Execute() - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) - } + // The capabilities subcommand runs without a DSN, so it builds the + // zero-value connector instead of validating the required flag. + configschema.RunConnector(ctx, "baton-postgresql", version, cfg.Config, getConnector, + connectorrunner.WithDefaultCapabilitiesConnectorBuilderV2(&connector.Postgresql{})) } -func getConnector(ctx context.Context, pgc *cfg.Postgresql) (types.ConnectorServer, error) { +func getConnector(ctx context.Context, pgc *cfg.Postgresql, _ *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) { l := ctxzap.Extract(ctx) cb, err := connector.New(ctx, pgc.Dsn, pgc.Schemas, pgc.IncludeColumns, pgc.IncludeLargeObjects, pgc.SyncAllDatabases, pgc.SkipBuiltInFunctions) if err != nil { l.Error("error creating connector", zap.Error(err)) - return nil, err - } - - newConnector, err := connectorbuilder.NewConnector(ctx, cb) - if err != nil { - l.Error("error creating connector", zap.Error(err)) - return nil, err + return nil, nil, err } - return newConnector, nil + return cb, nil, nil } diff --git a/pkg/connector/column.go b/pkg/connector/column.go index 8b8fd4ad..a46d6003 100644 --- a/pkg/connector/column.go +++ b/pkg/connector/column.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var columnResourceType = &v2.ResourceType{ @@ -26,30 +26,31 @@ func (r *columnSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return columnResourceType } -func (r *columnSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *columnSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken if parentResourceID == nil { - return nil, "", nil, nil + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != tableResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on column %s %s", parentResourceID.ResourceType, parentResourceID.Resource) + return nil, nil, fmt.Errorf("invalid parent resource ID on column %s %s", parentResourceID.ResourceType, parentResourceID.Resource) } db, parentID, err := parseWithDatabaseID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } columns, nextPageToken, err := client.ListColumns(ctx, parentID, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -67,46 +68,47 @@ func (r *columnSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *columnSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (r *columnSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { col := &postgres.ColumnModel{} - ens, err := entitlementsForPrivs(ctx, resource, col.AllPrivileges()) + ens, err := entitlementsForPrivs(ctx, res, col.AllPrivileges()) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *columnSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, tID, cID, err := parseColumnID(resource.Id.Resource) +func (r *columnSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, tID, cID, err := parseColumnID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } col, err := client.GetColumn(ctx, tID, cID) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, col) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, col) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func newColumnSyncer(ctx context.Context, c *postgres.ClientDatabasesPool) *columnSyncer { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index cf4e4dda..14fa3488 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -20,8 +20,8 @@ type Postgresql struct { skipBuiltInFunctions bool } -func (o *Postgresql) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncer { - return []connectorbuilder.ResourceSyncer{ +func (o *Postgresql) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncerV2 { + return []connectorbuilder.ResourceSyncerV2{ newRoleSyncer(ctx, o.clientPool.Default(ctx)), newSchemaSyncer(ctx, o.clientPool), newTableSyncer(ctx, o.clientPool, o.includeColumns), diff --git a/pkg/connector/database.go b/pkg/connector/database.go index e107a132..8973faf9 100644 --- a/pkg/connector/database.go +++ b/pkg/connector/database.go @@ -13,7 +13,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var databaseResourceType = &v2.ResourceType{ @@ -54,12 +54,13 @@ func (r *databaseSyncer) makeResource(ctx context.Context, dbModel *postgres.Dat } } -func (r *databaseSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *databaseSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { l := ctxzap.Extract(ctx) var err error + pToken := &opts.PageToken if parentResourceID != nil { - return nil, "", nil, fmt.Errorf("unexpected parent resource ID on database") + return nil, nil, fmt.Errorf("unexpected parent resource ID on database") } databases, nextPageToken, err := r.clientPool. @@ -67,7 +68,7 @@ func (r *databaseSyncer) List(ctx context.Context, parentResourceID *v2.Resource ListDatabases(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } defaultDbClient := r.clientPool.Default(ctx) @@ -96,24 +97,24 @@ func (r *databaseSyncer) List(ctx context.Context, parentResourceID *v2.Resource l.Warn("skipping database with error", zap.String("database", o.Name), zap.Error(err)) continue } - return nil, "", nil, err + return nil, nil, err } ret = append(ret, r.makeResource(ctx, o)) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *databaseSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - ens, err := entitlementsForPrivs(ctx, resource, postgres.Create|postgres.Temporary|postgres.Connect) +func (r *databaseSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + ens, err := entitlementsForPrivs(ctx, res, postgres.Create|postgres.Temporary|postgres.Connect) if err != nil { - return nil, "", nil, err + return nil, nil, err } ens = append(ens, &v2.Entitlement{ - Resource: resource, - Id: formatEntitlementID(resource, "superuser", false), + Resource: res, + Id: formatEntitlementID(res, "superuser", false), DisplayName: "Superuser", Description: "Has Superuser access", GrantableTo: []*v2.ResourceType{roleResourceType}, @@ -122,8 +123,8 @@ func (r *databaseSyncer) Entitlements(ctx context.Context, resource *v2.Resource }) ens = append(ens, &v2.Entitlement{ - Resource: resource, - Id: formatEntitlementID(resource, "create-db", false), + Resource: res, + Id: formatEntitlementID(res, "create-db", false), DisplayName: "Create Database", Description: "Can create new databases", GrantableTo: []*v2.ResourceType{roleResourceType}, @@ -132,8 +133,8 @@ func (r *databaseSyncer) Entitlements(ctx context.Context, resource *v2.Resource }) ens = append(ens, &v2.Entitlement{ - Resource: resource, - Id: formatEntitlementID(resource, "create-role", false), + Resource: res, + Id: formatEntitlementID(res, "create-role", false), DisplayName: "Create Role", Description: "Can create new roles", GrantableTo: []*v2.ResourceType{roleResourceType}, @@ -142,8 +143,8 @@ func (r *databaseSyncer) Entitlements(ctx context.Context, resource *v2.Resource }) ens = append(ens, &v2.Entitlement{ - Resource: resource, - Id: formatEntitlementID(resource, "bypass-rls", false), + Resource: res, + Id: formatEntitlementID(res, "bypass-rls", false), DisplayName: "Bypass RLS", Description: "Can bypass row level security options", GrantableTo: []*v2.ResourceType{roleResourceType}, @@ -152,8 +153,8 @@ func (r *databaseSyncer) Entitlements(ctx context.Context, resource *v2.Resource }) ens = append(ens, &v2.Entitlement{ - Resource: resource, - Id: formatEntitlementID(resource, "replication", false), + Resource: res, + Id: formatEntitlementID(res, "replication", false), DisplayName: "Replication", Description: "Can initiate replication connections, and create and drop replication slots", GrantableTo: []*v2.ResourceType{roleResourceType}, @@ -161,28 +162,29 @@ func (r *databaseSyncer) Entitlements(ctx context.Context, resource *v2.Resource Slug: "bypass rls", }) - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - rID, err := parseObjectID(resource.Id.Resource) +func (r *databaseSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + rID, err := parseObjectID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } db, err := r.client.GetDatabase(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := r.client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, r.client, resource, roles, db) + ret, err := roleGrantsForPrivileges(ctx, r.client, res, roles, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, r := range roles { @@ -194,11 +196,11 @@ func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pTok } if r.Superuser { - eID := formatEntitlementID(resource, "superuser", false) + eID := formatEntitlementID(res, "superuser", false) ret = append(ret, &v2.Grant{ Entitlement: &v2.Entitlement{ Id: eID, - Resource: resource, + Resource: res, }, Principal: principal, Id: formatGrantID(eID, principal.Id), @@ -206,11 +208,11 @@ func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pTok } if r.CreateDb { - eID := formatEntitlementID(resource, "create-db", false) + eID := formatEntitlementID(res, "create-db", false) ret = append(ret, &v2.Grant{ Entitlement: &v2.Entitlement{ Id: eID, - Resource: resource, + Resource: res, }, Principal: principal, Id: formatGrantID(eID, principal.Id), @@ -218,11 +220,11 @@ func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pTok } if r.CreateRole { - eID := formatEntitlementID(resource, "create-role", false) + eID := formatEntitlementID(res, "create-role", false) ret = append(ret, &v2.Grant{ Entitlement: &v2.Entitlement{ Id: eID, - Resource: resource, + Resource: res, }, Principal: principal, Id: formatGrantID(eID, principal.Id), @@ -230,11 +232,11 @@ func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pTok } if r.BypassRowSecurity { - eID := formatEntitlementID(resource, "bypass-rls", false) + eID := formatEntitlementID(res, "bypass-rls", false) ret = append(ret, &v2.Grant{ Entitlement: &v2.Entitlement{ Id: eID, - Resource: resource, + Resource: res, }, Principal: principal, Id: formatGrantID(eID, principal.Id), @@ -242,11 +244,11 @@ func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pTok } if r.Replication { - eID := formatEntitlementID(resource, "replication", false) + eID := formatEntitlementID(res, "replication", false) ret = append(ret, &v2.Grant{ Entitlement: &v2.Entitlement{ Id: eID, - Resource: resource, + Resource: res, }, Principal: principal, Id: formatGrantID(eID, principal.Id), @@ -254,7 +256,7 @@ func (r *databaseSyncer) Grants(ctx context.Context, resource *v2.Resource, pTok } } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *databaseSyncer) Create(ctx context.Context, resource *v2.Resource) (*v2.Resource, annotations.Annotations, error) { diff --git a/pkg/connector/function.go b/pkg/connector/function.go index 723f7761..6ae298ea 100644 --- a/pkg/connector/function.go +++ b/pkg/connector/function.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var functionResourceType = &v2.ResourceType{ @@ -27,30 +27,31 @@ func (r *functionSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return functionResourceType } -func (r *functionSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *functionSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken if parentResourceID == nil { - return nil, "", nil, nil + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != schemaResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on function") + return nil, nil, fmt.Errorf("invalid parent resource ID on function") } db, parentID, err := parseWithDatabaseID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } functions, nextPageToken, err := client.ListFunctions(ctx, parentID, r.skipBuiltInFunctions, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -68,13 +69,13 @@ func (r *functionSyncer) List(ctx context.Context, parentResourceID *v2.Resource }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *functionSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - dbId, _, err := parseWithDatabaseID(resource.Id.Resource) +func (r *functionSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + dbId, _, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } dbModel, err := r.clientPool. @@ -82,48 +83,49 @@ func (r *functionSyncer) Entitlements(ctx context.Context, resource *v2.Resource GetDatabaseById(ctx, dbId) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ens, err := entitlementsForPrivs(ctx, resource, postgres.Execute) + ens, err := entitlementsForPrivs(ctx, res, postgres.Execute) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, en := range ens { en.DisplayName = fmt.Sprintf("%s on %s", dbModel.Name, en.DisplayName) } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *functionSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, rID, err := parseWithDatabaseID(resource.Id.Resource) +func (r *functionSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, rID, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } function, err := client.GetFunction(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, function) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, function) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *functionSyncer) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) { diff --git a/pkg/connector/large_object.go b/pkg/connector/large_object.go index 8643eac8..c5ab68f5 100644 --- a/pkg/connector/large_object.go +++ b/pkg/connector/large_object.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var largeObjectResourceType = &v2.ResourceType{ @@ -27,20 +27,21 @@ func (r *largeObjectSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return largeObjectResourceType } -func (r *largeObjectSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *largeObjectSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken if parentResourceID != nil { - return nil, "", nil, fmt.Errorf("unexpected parent resource ID on large object") + return nil, nil, fmt.Errorf("unexpected parent resource ID on large object") } if !r.enabled { - return nil, "", nil, nil + return nil, &resource.SyncOpResults{}, nil } largeObjects, nextPageToken, err := r.client.ListLargeObjects(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -58,40 +59,41 @@ func (r *largeObjectSyncer) List(ctx context.Context, parentResourceID *v2.Resou }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *largeObjectSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - ens, err := entitlementsForPrivs(ctx, resource, postgres.Select|postgres.Update) +func (r *largeObjectSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + ens, err := entitlementsForPrivs(ctx, res, postgres.Select|postgres.Update) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *largeObjectSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - rID, err := parseObjectID(resource.Id.Resource) +func (r *largeObjectSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + rID, err := parseObjectID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } largeObject, err := r.client.GetLargeObject(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := r.client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, r.client, resource, roles, largeObject) + ret, err := roleGrantsForPrivileges(ctx, r.client, res, roles, largeObject) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func newLargeObjectSyncer(ctx context.Context, c *postgres.Client, enabled bool) *largeObjectSyncer { diff --git a/pkg/connector/procedure.go b/pkg/connector/procedure.go index bee7f90b..3e4fd5a5 100644 --- a/pkg/connector/procedure.go +++ b/pkg/connector/procedure.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var procedureResourceType = &v2.ResourceType{ @@ -26,30 +26,31 @@ func (r *procedureSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return procedureResourceType } -func (r *procedureSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *procedureSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken - if parentResourceID == nil || pToken == nil { - return nil, "", nil, nil + if parentResourceID == nil { + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != schemaResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on procedure") + return nil, nil, fmt.Errorf("invalid parent resource ID on procedure") } db, parentID, err := parseWithDatabaseID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } procedures, nextPageToken, err := client.ListProcedures(ctx, parentID, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -67,45 +68,46 @@ func (r *procedureSyncer) List(ctx context.Context, parentResourceID *v2.Resourc }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *procedureSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - ens, err := entitlementsForPrivs(ctx, resource, postgres.Execute) +func (r *procedureSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + ens, err := entitlementsForPrivs(ctx, res, postgres.Execute) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *procedureSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, rID, err := parseWithDatabaseID(resource.Id.Resource) +func (r *procedureSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, rID, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } procedure, err := client.GetProcedure(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, procedure) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, procedure) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *procedureSyncer) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) { diff --git a/pkg/connector/role.go b/pkg/connector/role.go index 342c74f8..e67a5b4a 100644 --- a/pkg/connector/role.go +++ b/pkg/connector/role.go @@ -11,7 +11,6 @@ import ( "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/crypto" - "github.com/conductorone/baton-sdk/pkg/pagination" sdkResource "github.com/conductorone/baton-sdk/pkg/types/resource" ) @@ -86,32 +85,33 @@ func (r *roleSyncer) makeResource(ctx context.Context, roleModel *postgres.RoleM }, nil } -func (r *roleSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *roleSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts sdkResource.SyncOpAttrs) ([]*v2.Resource, *sdkResource.SyncOpResults, error) { var err error + pToken := &opts.PageToken // if we ever support parentResourceID, be sure to set it in makeResource if parentResourceID != nil { - return nil, "", nil, fmt.Errorf("unexpected parent resource ID on role: %s", parentResourceID) + return nil, nil, fmt.Errorf("unexpected parent resource ID on role: %s", parentResourceID) } roles, nextPageToken, err := r.client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource for _, o := range roles { resource, err := r.makeResource(ctx, o) if err != nil { - return nil, "", nil, err + return nil, nil, err } ret = append(ret, resource) } - return ret, nextPageToken, nil, nil + return ret, &sdkResource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *roleSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (r *roleSyncer) Entitlements(ctx context.Context, resource *v2.Resource, _ sdkResource.SyncOpAttrs) ([]*v2.Entitlement, *sdkResource.SyncOpResults, error) { var ret []*v2.Entitlement annos := annotations.Annotations(resource.Annotations) @@ -119,7 +119,7 @@ func (r *roleSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pT gt := &v2.GroupTrait{} ok, err := annos.Pick(gt) if err != nil { - return nil, "", nil, err + return nil, nil, err } if ok { @@ -143,32 +143,33 @@ func (r *roleSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pT }) } - return ret, "", nil, nil + return ret, &sdkResource.SyncOpResults{}, nil } -func (r *roleSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { +func (r *roleSyncer) Grants(ctx context.Context, resource *v2.Resource, opts sdkResource.SyncOpAttrs) ([]*v2.Grant, *sdkResource.SyncOpResults, error) { var ret []*v2.Grant + pToken := &opts.PageToken annos := annotations.Annotations(resource.Annotations) gt := &v2.GroupTrait{} ok, err := annos.Pick(gt) if err != nil { - return nil, "", nil, err + return nil, nil, err } // Roles only have entitlements if they are a group if !ok { - return nil, "", nil, nil + return nil, &sdkResource.SyncOpResults{}, nil } roleID, err := parseObjectID(resource.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } roleMembers, nextPageToken, err := r.client.ListRoleMembers(ctx, roleID, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var eID string @@ -195,7 +196,7 @@ func (r *roleSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken * }) } - return ret, nextPageToken, nil, nil + return ret, &sdkResource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *roleSyncer) Delete(ctx context.Context, resourceId *v2.ResourceId) (annotations.Annotations, error) { diff --git a/pkg/connector/schema.go b/pkg/connector/schema.go index 91590957..d60da723 100644 --- a/pkg/connector/schema.go +++ b/pkg/connector/schema.go @@ -8,7 +8,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var schemaResourceType = &v2.ResourceType{ @@ -27,34 +27,35 @@ func (r *schemaSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return schemaResourceType } -func (r *schemaSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *schemaSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken if parentResourceID == nil { - return nil, "", nil, nil + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != databaseResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on schema") + return nil, nil, fmt.Errorf("invalid parent resource ID on schema") } dbId, err := parseObjectID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, dbName, err := r.clientPool.Get(ctx, strconv.Itoa(int(dbId))) if err != nil { - return nil, "", nil, err + return nil, nil, err } if dbName == "" { - return nil, "", nil, fmt.Errorf("database name not found for ID %d", dbId) + return nil, nil, fmt.Errorf("database name not found for ID %d", dbId) } schemas, nextPageToken, err := client.ListSchemas(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -78,45 +79,46 @@ func (r *schemaSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *schemaSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - ens, err := entitlementsForPrivs(ctx, resource, postgres.Usage|postgres.Create) +func (r *schemaSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + ens, err := entitlementsForPrivs(ctx, res, postgres.Usage|postgres.Create) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *schemaSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, rID, err := parseWithDatabaseID(resource.Id.Resource) +func (r *schemaSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, rID, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } schema, err := client.GetSchema(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, schema) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, schema) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func newSchemaSyncer(ctx context.Context, c *postgres.ClientDatabasesPool) *schemaSyncer { diff --git a/pkg/connector/sequence.go b/pkg/connector/sequence.go index 77b56e56..64b10fd6 100644 --- a/pkg/connector/sequence.go +++ b/pkg/connector/sequence.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var sequenceResourceType = &v2.ResourceType{ @@ -26,30 +26,31 @@ func (r *sequenceSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return sequenceResourceType } -func (r *sequenceSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *sequenceSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken - if parentResourceID == nil || pToken == nil { - return nil, "", nil, nil + if parentResourceID == nil { + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != schemaResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on sequence") + return nil, nil, fmt.Errorf("invalid parent resource ID on sequence") } db, parentID, err := parseWithDatabaseID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } sequences, nextPageToken, err := client.ListSequences(ctx, parentID, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -67,13 +68,13 @@ func (r *sequenceSyncer) List(ctx context.Context, parentResourceID *v2.Resource }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *sequenceSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - dbId, _, err := parseWithDatabaseID(resource.Id.Resource) +func (r *sequenceSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + dbId, _, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } dbModel, err := r.clientPool. @@ -81,52 +82,53 @@ func (r *sequenceSyncer) Entitlements(ctx context.Context, resource *v2.Resource GetDatabaseById(ctx, dbId) if err != nil { - return nil, "", nil, err + return nil, nil, err } ens, err := entitlementsForPrivs( ctx, - resource, + res, postgres.Select|postgres.Update|postgres.Usage, ) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, en := range ens { en.DisplayName = fmt.Sprintf("%s on %s", dbModel.Name, en.DisplayName) } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *sequenceSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, rID, err := parseWithDatabaseID(resource.Id.Resource) +func (r *sequenceSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, rID, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } sequence, err := client.GetSequence(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, sequence) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, sequence) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *sequenceSyncer) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) { diff --git a/pkg/connector/table.go b/pkg/connector/table.go index 0719adb8..856a373d 100644 --- a/pkg/connector/table.go +++ b/pkg/connector/table.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var tableResourceType = &v2.ResourceType{ @@ -27,35 +27,36 @@ func (r *tableSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return tableResourceType } -func (r *tableSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *tableSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken - if parentResourceID == nil || pToken == nil { - return nil, "", nil, nil + if parentResourceID == nil { + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != schemaResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on table") + return nil, nil, fmt.Errorf("invalid parent resource ID on table") } database, parentID, err := parseWithDatabaseID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, database) if err != nil { - return nil, "", nil, err + return nil, nil, err } schema, err := client.GetSchema(ctx, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } tables, nextPageToken, err := client.ListTables(ctx, schema.Name, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -77,13 +78,13 @@ func (r *tableSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *tableSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - dbId, _, err := parseWithDatabaseID(resource.Id.Resource) +func (r *tableSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + dbId, _, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } dbModel, err := r.clientPool. @@ -91,52 +92,53 @@ func (r *tableSyncer) Entitlements(ctx context.Context, resource *v2.Resource, p GetDatabaseById(ctx, dbId) if err != nil { - return nil, "", nil, err + return nil, nil, err } ens, err := entitlementsForPrivs( ctx, - resource, + res, postgres.Select|postgres.Insert|postgres.Update|postgres.Delete|postgres.Truncate|postgres.Trigger|postgres.References, ) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, en := range ens { - en.DisplayName = fmt.Sprintf("%s - %s", dbModel.Name, resource.DisplayName) + en.DisplayName = fmt.Sprintf("%s - %s", dbModel.Name, res.DisplayName) } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *tableSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, rID, err := parseWithDatabaseID(resource.Id.Resource) +func (r *tableSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, rID, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } table, err := client.GetTable(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, table) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, table) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *tableSyncer) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) { diff --git a/pkg/connector/view.go b/pkg/connector/view.go index 9fcb8936..e09a4c0b 100644 --- a/pkg/connector/view.go +++ b/pkg/connector/view.go @@ -7,7 +7,7 @@ import ( "github.com/conductorone/baton-postgresql/pkg/postgres" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/types/resource" ) var viewResourceType = &v2.ResourceType{ @@ -26,30 +26,31 @@ func (r *viewSyncer) ResourceType(ctx context.Context) *v2.ResourceType { return viewResourceType } -func (r *viewSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (r *viewSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { var err error + pToken := &opts.PageToken if parentResourceID == nil { - return nil, "", nil, nil + return nil, &resource.SyncOpResults{}, nil } if parentResourceID.ResourceType != schemaResourceType.Id { - return nil, "", nil, fmt.Errorf("invalid parent resource ID on view") + return nil, nil, fmt.Errorf("invalid parent resource ID on view") } db, parentID, err := parseWithDatabaseID(parentResourceID.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } views, nextPageToken, err := client.ListViews(ctx, parentID, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -67,13 +68,13 @@ func (r *viewSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, }) } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } -func (r *viewSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - dbId, _, err := parseWithDatabaseID(resource.Id.Resource) +func (r *viewSyncer) Entitlements(ctx context.Context, res *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + dbId, _, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } dbModel, err := r.clientPool. @@ -81,52 +82,53 @@ func (r *viewSyncer) Entitlements(ctx context.Context, resource *v2.Resource, pT GetDatabaseById(ctx, dbId) if err != nil { - return nil, "", nil, err + return nil, nil, err } ens, err := entitlementsForPrivs( ctx, - resource, + res, postgres.Select|postgres.Insert|postgres.Update|postgres.Delete|postgres.Truncate|postgres.Trigger|postgres.References, ) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, en := range ens { en.DisplayName = fmt.Sprintf("%s on %s", dbModel.Name, en.DisplayName) } - return ens, "", nil, nil + return ens, &resource.SyncOpResults{}, nil } -func (r *viewSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - db, rID, err := parseWithDatabaseID(resource.Id.Resource) +func (r *viewSyncer) Grants(ctx context.Context, res *v2.Resource, opts resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + pToken := &opts.PageToken + db, rID, err := parseWithDatabaseID(res.Id.Resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } client, _, err := r.clientPool.Get(ctx, db) if err != nil { - return nil, "", nil, err + return nil, nil, err } view, err := client.GetView(ctx, rID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, nextPageToken, err := client.ListRoles(ctx, &postgres.Pager{Token: pToken.Token, Size: pToken.Size}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - ret, err := roleGrantsForPrivileges(ctx, client, resource, roles, view) + ret, err := roleGrantsForPrivileges(ctx, client, res, roles, view) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return ret, nextPageToken, nil, nil + return ret, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil } func (r *viewSyncer) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) { diff --git a/scripts/get-baton.sh b/scripts/get-baton.sh deleted file mode 100755 index bc072bec..00000000 --- a/scripts/get-baton.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m) -if [ "${ARCH}" = "x86_64" ]; then - ARCH="amd64" -elif [ "${ARCH}" = "aarch64" ]; then - ARCH="arm64" -fi - -# Use the CLI shipped with the connector's SDK so it can read the same C1Z -# format, including Pebble-backed files. -SDK_VERSION=$(go list -m -f '{{.Version}}' github.com/conductorone/baton-sdk) -BASE_URL="https://github.com/conductorone/baton-sdk/releases/download" - -EXTENSION="tar.gz" -if [ "${OS}" = "darwin" ]; then - EXTENSION="zip" -fi -FILENAME="baton-${SDK_VERSION}-${OS}-${ARCH}.${EXTENSION}" -DOWNLOAD_URL="${BASE_URL}/${SDK_VERSION}/${FILENAME}" - -curl -fSL -o "${FILENAME}" "${DOWNLOAD_URL}" -CHECKSUMS="baton_${SDK_VERSION#v}_checksums.txt" -curl -fSL -o "${CHECKSUMS}" "${BASE_URL}/${SDK_VERSION}/${CHECKSUMS}" - -if command -v sha256sum >/dev/null 2>&1; then - CHECKSUM_COMMAND=(sha256sum) -else - CHECKSUM_COMMAND=(shasum -a 256) -fi -# Check this archive specifically; unrelated files left by earlier installs -# must not let a missing checksum for the selected archive pass verification. -awk -v filename="${FILENAME}" '$2 == filename { print; found = 1 } END { exit !found }' "${CHECKSUMS}" | - "${CHECKSUM_COMMAND[@]}" --check - - -if [ "${EXTENSION}" = "zip" ]; then - unzip -o "${FILENAME}" -else - tar xzf "${FILENAME}" -fi From cbb02f3c83e81e7fb52a1f1dd4c204b7c65cad76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Gonz=C3=A1lez?= Date: Thu, 10 Sep 2026 16:29:38 -0700 Subject: [PATCH 2/4] Run integration tests through the shared connector actions Replace the hand-rolled grant/revoke and create/delete steps with the sync-test and account-provisioning actions from github-workflows. Together they cover what the old steps did plus idempotent double grant and double revoke, and credential rotation on the created account, which had no automated coverage before. The actions fetch the baton CLI themselves, so the install step goes with the script the previous commit removed. The account-provisioning action requires an email; the SDK validates it as an address and this connector names the created role after it, so the lookup goes by email. The storage-engine override is dropped since pebble is the default. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yaml | 78 +++++++++------------------------------ 1 file changed, 17 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7129d999..e36bf1f8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,9 +50,7 @@ jobs: POSTGRES_PASSWORD: secretpassword env: BATON_LOG_LEVEL: debug - BATON_STORAGE_ENGINE: pebble BATON_DSN: "postgres://postgres:secretpassword@localhost:5432/postgres" - CONNECTOR_GRANT: "grant:entitlement:role:3375:member:role:10" CONNECTOR_ENTITLEMENT: "entitlement:role:3375:member" CONNECTOR_PRINCIPAL: "role:10" CONNECTOR_PRINCIPAL_TYPE: "role" @@ -70,64 +68,22 @@ jobs: # env: # PGPASSWORD: secretpassword # run: psql -h localhost --user postgres -f test/ci.sql - - name: Install baton - run: ./scripts/get-baton.sh && mv baton /usr/local/bin - name: Build baton-postgresql run: go build ./cmd/baton-postgresql - - name: Run baton-postgresql - run: ./baton-postgresql - - - name: Grant entitlement - run: ./baton-postgresql --grant-entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" --grant-principal "${{ env.CONNECTOR_PRINCIPAL }}" --grant-principal-type "${{ env.CONNECTOR_PRINCIPAL_TYPE }}" - - - name: Check grant was granted - run: ./baton-postgresql && baton grants --entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" --output-format=json | jq --exit-status --arg principal "${{ env.CONNECTOR_PRINCIPAL }}" 'any(.grants[]?; .principal.id.resource == $principal)' - - - name: Revoke grant - run: ./baton-postgresql --revoke-grant "${{ env.CONNECTOR_GRANT }}" - - - name: Check grant was revoked - run: ./baton-postgresql && baton grants --entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" --output-format=json | jq --exit-status --arg principal "${{ env.CONNECTOR_PRINCIPAL }}" 'any(.grants[]?; .principal.id.resource == $principal) | not' - - - name: Grant entitlement - run: ./baton-postgresql --grant-entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" --grant-principal "${{ env.CONNECTOR_PRINCIPAL }}" --grant-principal-type "${{ env.CONNECTOR_PRINCIPAL_TYPE }}" - - - name: Check grant was re-granted - run: ./baton-postgresql && baton grants --entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" --output-format=json | jq --exit-status --arg principal "${{ env.CONNECTOR_PRINCIPAL }}" 'any(.grants[]?; .principal.id.resource == $principal)' - - - name: Create user - run: ./baton-postgresql --create-account-login "${{ env.CONNECTOR_NEW_USER }}" - - - name: Check user was created - run: ./baton-postgresql && baton resources -o json | jq -e --arg login "${{ env.CONNECTOR_NEW_USER }}" 'any(.resources[].resource.annotations[]?;.["@type"]=="type.googleapis.com/c1.connector.v2.UserTrait" and .login==$login)' - - - name: Fetch user id - shell: bash - run: | - set -euo pipefail - NEW_USER_ID=$(baton resources -t role -o json | jq -r --arg login "${{ env.CONNECTOR_NEW_USER }}" '.resources[].resource | select(any(.annotations[]?; .["@type"]=="type.googleapis.com/c1.connector.v2.UserTrait" and .login==$login)) | .id.resource') - if [ -z "$NEW_USER_ID" ]; then - echo "No role found for login '$CONNECTOR_NEW_USER'" >&2 - exit 1 - fi - if [ "$(printf '%s\n' "$NEW_USER_ID" | wc -l)" -ne 1 ]; then - echo "Expected exactly one role for login '$CONNECTOR_NEW_USER', but found multiple matches" >&2 - exit 1 - fi - echo "NEW_USER_ID=$NEW_USER_ID" >> "$GITHUB_ENV" - - - name: Grant role to user - run: ./baton-postgresql --grant-entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" --grant-principal "${{ env.NEW_USER_ID }}" --grant-principal-type "${{ env.CONNECTOR_PRINCIPAL_TYPE }}" - - - name: Check role was granted - run: ./baton-postgresql && baton grants --entitlement "${{ env.CONNECTOR_ENTITLEMENT }}" -o json | jq -e --arg login "${{ env.CONNECTOR_NEW_USER }}" 'any(.grants[]?; any(.principal.annotations[]?; .["@type"]=="type.googleapis.com/c1.connector.v2.UserTrait" and .login==$login) or any(.grant.principal.annotations[]?; .["@type"]=="type.googleapis.com/c1.connector.v2.UserTrait" and .login==$login))' - - - name: Delete user - run: ./baton-postgresql --delete-resource "${{ env.NEW_USER_ID }}" --delete-resource-type "${{ env.CONNECTOR_PRINCIPAL_TYPE }}" - - - name: Check user was deleted - run: ./baton-postgresql && baton resources -o json | jq -e --arg login "${{ env.CONNECTOR_NEW_USER }}" 'any(.resources[].resource.annotations[]?;.["@type"]=="type.googleapis.com/c1.connector.v2.UserTrait" and .login==$login) | not' - - # TODO: get correct role id using baton CLI - # - name: Rotate credentials for user - # run: ./baton-postgresql --rotate-credentials 'role:16384' --rotate-credentials-type 'role' + - name: Test connector syncing + uses: ConductorOne/github-workflows/actions/sync-test@v4 + with: + connector: ./baton-postgresql + baton-entitlement: ${{ env.CONNECTOR_ENTITLEMENT }} + baton-principal: ${{ env.CONNECTOR_PRINCIPAL }} + baton-principal-type: ${{ env.CONNECTOR_PRINCIPAL_TYPE }} + - name: Test account provisioning + uses: ConductorOne/github-workflows/actions/account-provisioning@v4 + with: + connector: ./baton-postgresql + account-login: ${{ env.CONNECTOR_NEW_USER }} + # The SDK validates this as an address, and the connector names the + # created role after it, so the lookup has to go by email. + account-email: ${{ env.CONNECTOR_NEW_USER }}@example.com + account-type: ${{ env.CONNECTOR_PRINCIPAL_TYPE }} + search-method: email From 99b6536b5e49be480acc0646c9f8965c3ed1532c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Gonz=C3=A1lez?= Date: Fri, 11 Sep 2026 09:41:55 -0700 Subject: [PATCH 3/4] Classify auth failures and enable the CI unauthenticated-sync probe Review follow-ups on the shared-actions change. - postgres.New attaches a gRPC status to connection failures the server attributes to the client's authorization: SQLSTATE class 28 maps to Unauthenticated (exit 16) and 42501 to PermissionDenied (exit 7). The original pgx error stays in the chain. - ci.yaml hands the sync-test auth-error check a bad DSN through bad-credentials; it was skipping because BATON_DSN is not credential-shaped. Document why the shared actions' unpinned baton CLI is acceptable for reading this connector's c1z files. - getConnector no longer logs the error it returns; RunConnector prints it on exit and the pool constructor already logs the failure. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yaml | 10 ++++++++ cmd/baton-postgresql/main.go | 7 ++--- pkg/postgres/client.go | 28 +++++++++++++++++++- pkg/postgres/client_test.go | 50 ++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 pkg/postgres/client_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e36bf1f8..00cf9421 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,6 +70,11 @@ jobs: # run: psql -h localhost --user postgres -f test/ci.sql - name: Build baton-postgresql run: go build ./cmd/baton-postgresql + # Both shared actions install the latest released baton CLI; the action + # has no version pin or checksum. That is fine for reading this + # connector's c1z files: a newer CLI reads older files, and the pinned + # SDK is a released version, so the latest CLI is at least as new as the + # connector's c1z writer once that release's assets are published. - name: Test connector syncing uses: ConductorOne/github-workflows/actions/sync-test@v4 with: @@ -77,6 +82,11 @@ jobs: baton-entitlement: ${{ env.CONNECTOR_ENTITLEMENT }} baton-principal: ${{ env.CONNECTOR_PRINCIPAL }} baton-principal-type: ${{ env.CONNECTOR_PRINCIPAL_TYPE }} + # The unauthenticated-sync check only auto-invalidates credential- + # looking variables, and BATON_DSN is not one, so hand it a bad DSN. + # The connector maps the resulting SQLSTATE 28P01 to Unauthenticated + # (exit 16), which is what the check accepts. + bad-credentials: BATON_DSN=postgres://bad:bad@localhost:5432/postgres - name: Test account provisioning uses: ConductorOne/github-workflows/actions/account-provisioning@v4 with: diff --git a/cmd/baton-postgresql/main.go b/cmd/baton-postgresql/main.go index 4c5c24f8..ef953b02 100644 --- a/cmd/baton-postgresql/main.go +++ b/cmd/baton-postgresql/main.go @@ -7,8 +7,6 @@ import ( "github.com/conductorone/baton-sdk/pkg/cli" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/connectorrunner" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" "github.com/conductorone/baton-postgresql/pkg/connector" configschema "github.com/conductorone/baton-sdk/pkg/config" @@ -26,11 +24,10 @@ func main() { } func getConnector(ctx context.Context, pgc *cfg.Postgresql, _ *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) { - l := ctxzap.Extract(ctx) - + // No log here: RunConnector prints the returned error on exit, and the + // pool constructor already logs the connection failure. cb, err := connector.New(ctx, pgc.Dsn, pgc.Schemas, pgc.IncludeColumns, pgc.IncludeLargeObjects, pgc.SyncAllDatabases, pgc.SkipBuiltInFunctions) if err != nil { - l.Error("error creating connector", zap.Error(err)) return nil, nil, err } diff --git a/pkg/postgres/client.go b/pkg/postgres/client.go index b394dd66..5bdcf6d5 100644 --- a/pkg/postgres/client.go +++ b/pkg/postgres/client.go @@ -2,11 +2,17 @@ package postgres import ( "context" + "errors" + "fmt" + "strings" "sync" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/jackc/pgconn" "github.com/jackc/pgx/v4/pgxpool" "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) const withGrantOptions = " WITH GRANT OPTION" @@ -123,6 +129,26 @@ func WithSchemaFilter(filter []string) ClientOpt { } } +// classifyConnectError attaches a gRPC status to connection failures the +// server attributes to the client's authorization, so the process exit code +// reports a configuration problem instead of an unknown error. SQLSTATE +// class 28 is "invalid authorization specification" (bad user or password, +// or no matching pg_hba.conf entry); 42501 is "insufficient privilege" (no +// CONNECT on the database). The original error stays in the chain. +func classifyConnectError(err error) error { + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + return err + } + switch { + case strings.HasPrefix(pgErr.Code, "28"): + return fmt.Errorf("%w: %w", status.Error(codes.Unauthenticated, "postgres authentication failed"), err) + case pgErr.Code == "42501": + return fmt.Errorf("%w: %w", status.Error(codes.PermissionDenied, "postgres permission denied"), err) + } + return err +} + func New(ctx context.Context, dsn string, opts ...ClientOpt) (*Client, error) { l := ctxzap.Extract(ctx) @@ -137,7 +163,7 @@ func New(ctx context.Context, dsn string, opts ...ClientOpt) (*Client, error) { db, err := pgxpool.ConnectConfig(ctx, config) if err != nil { - return nil, err + return nil, classifyConnectError(err) } c := &Client{ diff --git a/pkg/postgres/client_test.go b/pkg/postgres/client_test.go new file mode 100644 index 00000000..a5df1e31 --- /dev/null +++ b/pkg/postgres/client_test.go @@ -0,0 +1,50 @@ +package postgres + +import ( + "errors" + "fmt" + "testing" + + "github.com/jackc/pgconn" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestClassifyConnectError(t *testing.T) { + cases := []struct { + name string + sqlstate string + want codes.Code + }{ + {name: "invalid password", sqlstate: "28P01", want: codes.Unauthenticated}, + {name: "invalid authorization specification", sqlstate: "28000", want: codes.Unauthenticated}, + {name: "insufficient privilege", sqlstate: "42501", want: codes.PermissionDenied}, + {name: "unrelated server error", sqlstate: "3D000", want: codes.Unknown}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // pgx wraps the server error the same way ConnectConfig does. + pgErr := &pgconn.PgError{Code: tc.sqlstate, Message: "server said no"} + err := fmt.Errorf("failed to connect: %w", pgErr) + + got := classifyConnectError(err) + + require.Equal(t, tc.want, status.Code(got)) + var unwrapped *pgconn.PgError + require.True(t, errors.As(got, &unwrapped), "original PgError must stay in the chain") + require.Equal(t, tc.sqlstate, unwrapped.Code) + require.Contains(t, got.Error(), "server said no") + }) + } +} + +func TestClassifyConnectErrorPassesThroughNonServerErrors(t *testing.T) { + err := errors.New("dial tcp: connection refused") + + got := classifyConnectError(err) + + require.Same(t, err, got) + require.Equal(t, codes.Unknown, status.Code(got)) +} From a5cec0813b0c6f09920673c3b29db94d9c9fa035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Gonz=C3=A1lez?= Date: Fri, 11 Sep 2026 10:04:16 -0700 Subject: [PATCH 4/4] Classify secondary database connection failures and remove duplicate logging --- cmd/baton-postgresql/main.go | 3 +-- pkg/postgres/client.go | 5 +---- pkg/postgres/client_test.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cmd/baton-postgresql/main.go b/cmd/baton-postgresql/main.go index ef953b02..8a0023ea 100644 --- a/cmd/baton-postgresql/main.go +++ b/cmd/baton-postgresql/main.go @@ -24,8 +24,7 @@ func main() { } func getConnector(ctx context.Context, pgc *cfg.Postgresql, _ *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) { - // No log here: RunConnector prints the returned error on exit, and the - // pool constructor already logs the connection failure. + // RunConnector prints the returned error on exit, so don't log it here. cb, err := connector.New(ctx, pgc.Dsn, pgc.Schemas, pgc.IncludeColumns, pgc.IncludeLargeObjects, pgc.SyncAllDatabases, pgc.SkipBuiltInFunctions) if err != nil { return nil, nil, err diff --git a/pkg/postgres/client.go b/pkg/postgres/client.go index 5bdcf6d5..488c5713 100644 --- a/pkg/postgres/client.go +++ b/pkg/postgres/client.go @@ -27,11 +27,8 @@ type ClientDatabasesPool struct { } func NewClientDatabasesPool(ctx context.Context, dsn string, opts ...ClientOpt) (*ClientDatabasesPool, error) { - l := ctxzap.Extract(ctx) - defaultClientDsn, err := New(ctx, dsn, opts...) if err != nil { - l.Error("failed to create default database client", zap.Error(err)) return nil, err } @@ -89,7 +86,7 @@ func (p *ClientDatabasesPool) Get(ctx context.Context, database string) (*Client db, err := pgxpool.ConnectConfig(ctx, config) if err != nil { - return nil, "", err + return nil, "", classifyConnectError(err) } c := &Client{ diff --git a/pkg/postgres/client_test.go b/pkg/postgres/client_test.go index a5df1e31..679805b7 100644 --- a/pkg/postgres/client_test.go +++ b/pkg/postgres/client_test.go @@ -1,16 +1,51 @@ package postgres import ( + "context" "errors" "fmt" + "net/url" "testing" + "github.com/conductorone/baton-postgresql/pkg/testutil" "github.com/jackc/pgconn" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +func TestClientDatabasesPoolGetPermissionDenied(t *testing.T) { + ctx := context.Background() + container := testutil.SetupPostgresContainer(ctx, t) + t.Cleanup(func() { + container.Db().Close() + require.NoError(t, container.Container().Terminate(ctx)) + }) + + _, err := container.Db().Exec(ctx, "CREATE ROLE limited_login LOGIN PASSWORD 'test-password'") + require.NoError(t, err) + _, err = container.Db().Exec(ctx, "CREATE DATABASE restricted_database") + require.NoError(t, err) + _, err = container.Db().Exec(ctx, "REVOKE CONNECT ON DATABASE restricted_database FROM PUBLIC") + require.NoError(t, err) + + dsn, err := url.Parse(container.Dsn()) + require.NoError(t, err) + dsn.User = url.UserPassword("limited_login", "test-password") + pool, err := NewClientDatabasesPool(ctx, dsn.String()) + require.NoError(t, err) + t.Cleanup(pool.Default(ctx).db.Close) + + database, err := pool.Default(ctx).GetDatabaseByName(ctx, "restricted_database") + require.NoError(t, err) + client, _, err := pool.Get(ctx, fmt.Sprint(database.ID)) + require.Nil(t, client) + require.Equal(t, codes.PermissionDenied, status.Code(err)) + var pgErr *pgconn.PgError + require.ErrorAs(t, err, &pgErr) + require.Equal(t, "42501", pgErr.Code) +} + func TestClassifyConnectError(t *testing.T) { cases := []struct { name string