diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7129d999..00cf9421 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,32 @@ 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' + # 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: + connector: ./baton-postgresql + 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: + 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 diff --git a/cmd/baton-postgresql/main.go b/cmd/baton-postgresql/main.go index b0c6acec..8a0023ea 100644 --- a/cmd/baton-postgresql/main.go +++ b/cmd/baton-postgresql/main.go @@ -2,15 +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" "github.com/conductorone/baton-postgresql/pkg/connector" configschema "github.com/conductorone/baton-sdk/pkg/config" @@ -21,38 +17,18 @@ 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) { - l := ctxzap.Extract(ctx) - +func getConnector(ctx context.Context, pgc *cfg.Postgresql, _ *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) { + // 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 { - 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/pkg/postgres/client.go b/pkg/postgres/client.go index b394dd66..488c5713 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" @@ -21,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 } @@ -83,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{ @@ -123,6 +126,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 +160,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..679805b7 --- /dev/null +++ b/pkg/postgres/client_test.go @@ -0,0 +1,85 @@ +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 + 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)) +} 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