diff --git a/Makefile b/Makefile index 3d25486e..9d1879b1 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,30 @@ GOOS = $(shell go env GOOS) GOARCH = $(shell go env GOARCH) BUILD_DIR = dist/${GOOS}_${GOARCH} +GENERATED_CONF := pkg/config/conf.gen.go + +ifeq ($(GOOS),windows) +OUTPUT_PATH = ${BUILD_DIR}/baton-cloudflare.exe +else OUTPUT_PATH = ${BUILD_DIR}/baton-cloudflare -GENERATED_CONF = pkg/config/conf.gen.go +endif -.PHONY: generate -generate: $(GENERATED_CONF) +# Set the build tag conditionally based on ENABLE_LAMBDA +ifdef BATON_LAMBDA_SUPPORT + BUILD_TAGS=-tags baton_lambda_support +else + BUILD_TAGS= +endif + +.PHONY: build +build: $(GENERATED_CONF) + go build ${BUILD_TAGS} -o ${OUTPUT_PATH} ./cmd/baton-cloudflare $(GENERATED_CONF): pkg/config/config.go go.mod + @echo "Generating $(GENERATED_CONF)..." go generate ./pkg/config -.PHONY: build -build: $(GENERATED_CONF) - rm -f ${OUTPUT_PATH} - mkdir -p ${BUILD_DIR} - go build -o ${OUTPUT_PATH} cmd/baton-cloudflare/*.go +generate: $(GENERATED_CONF) .PHONY: update-deps update-deps: @@ -22,8 +32,8 @@ update-deps: go mod tidy -v go mod vendor -.PHONY: add-dep -add-dep: +.PHONY: add-deps +add-deps: go mod tidy -v go mod vendor diff --git a/cmd/baton-cloudflare/config.go b/cmd/baton-cloudflare/config.go deleted file mode 100644 index c1aae7ad..00000000 --- a/cmd/baton-cloudflare/config.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "github.com/conductorone/baton-sdk/pkg/field" -) - -var ( - apiKeyField = field.StringField( - "api-key", - field.WithDescription("The api key for the Cloudflare account."), - ) - apiTokenField = field.StringField( - "api-token", - field.WithDescription("The api token for the Cloudflare account."), - ) - accountIdField = field.StringField( - "account-id", - field.WithRequired(true), - field.WithDescription("The account id for the Cloudflare account."), - ) - emailIdField = field.StringField( - "email-id", - field.WithDescription("The email id for the Cloudflare account."), - ) - configurationFields = []field.SchemaField{ - apiKeyField, - apiTokenField, - accountIdField, - emailIdField, - } - fieldRelationships = []field.SchemaFieldRelationship{ - field.FieldsAtLeastOneUsed(apiTokenField, apiKeyField), - field.FieldsDependentOn( - []field.SchemaField{apiKeyField}, - []field.SchemaField{emailIdField}, - ), - } -) diff --git a/cmd/baton-cloudflare/config_test.go b/cmd/baton-cloudflare/config_test.go deleted file mode 100644 index b0c0f0bf..00000000 --- a/cmd/baton-cloudflare/config_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "testing" - - "github.com/conductorone/baton-sdk/pkg/field" - "github.com/conductorone/baton-sdk/pkg/test" - "github.com/conductorone/baton-sdk/pkg/ustrings" -) - -func TestConfigs(t *testing.T) { - testCases := []test.TestCaseFromExpression{ - { - "", - false, - "empty configs", - }, - { - "--account-id 1", - false, - "missing api key or api token", - }, - { - "--account-id --api-token 1", - true, - "with api token", - }, - { - "--account-id --api-key 1", - false, - "with api key but missing email ID", - }, - { - "--account-id --api-key 1 --email-id 1", - true, - "with api key", - }, - { - "--account-id --api-key 1 --api-token 1", - false, - "api key and api token", - }, - } - - test.ExerciseTestCasesFromExpressions( - t, - field.Configuration{ - Fields: configurationFields, - Constraints: fieldRelationships, - }, - nil, - ustrings.ParseFlags, - testCases, - ) -} diff --git a/cmd/baton-cloudflare/main.go b/cmd/baton-cloudflare/main.go index 8968d9d7..76e221d5 100644 --- a/cmd/baton-cloudflare/main.go +++ b/cmd/baton-cloudflare/main.go @@ -2,66 +2,22 @@ package main import ( "context" - "fmt" - "os" cfg "github.com/conductorone/baton-cloudflare/pkg/config" "github.com/conductorone/baton-cloudflare/pkg/connector" "github.com/conductorone/baton-sdk/pkg/config" - "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" ) -const ( - version = "dev" - connectorName = "baton-cloudflare" -) +var version = "dev" func main() { ctx := context.Background() - _, cmd, err := config.DefineConfiguration( - ctx, - connectorName, - getConnector, + config.RunConnector(ctx, + "baton-cloudflare", + version, cfg.Config, - connectorrunner.WithDefaultCapabilitiesConnectorBuilder(&connector.Cloudflare{}), + connector.New, + connectorrunner.WithDefaultCapabilitiesConnectorBuilderV2(&connector.Cloudflare{}), ) - 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) - } -} - -func getConnector(ctx context.Context, c *cfg.Cloudflare) (types.ConnectorServer, error) { - l := ctxzap.Extract(ctx) - connConfig := connector.Config{ - AccountId: c.AccountId, - ApiToken: c.ApiToken, - EmailId: c.EmailId, - ApiKey: c.ApiKey, - } - - cb, err := connector.New(ctx, connConfig) - if err != nil { - l.Error("error creating connector", zap.Error(err)) - return nil, err - } - - conn, err := connectorbuilder.NewConnector(ctx, cb) - if err != nil { - l.Error("error creating connector", zap.Error(err)) - return nil, err - } - - return conn, nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index 3f4ab0b1..f8a9f4ef 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,43 +5,59 @@ import ( ) var ( - ApiKeyField = field.StringField( + apiKeyField = field.StringField( "api-key", - field.WithIsSecret(true), + field.WithDisplayName("API Key"), field.WithDescription("The api key for the Cloudflare account."), + field.WithIsSecret(true), + field.WithRequired(true), ) - ApiTokenField = field.StringField( + apiTokenField = field.StringField( "api-token", - field.WithIsSecret(true), + field.WithDisplayName("API Token"), field.WithDescription("The api token for the Cloudflare account."), + field.WithIsSecret(true), + field.WithRequired(true), ) - AccountIdField = field.StringField( + accountIdField = field.StringField( "account-id", - field.WithRequired(true), + field.WithDisplayName("Account ID"), field.WithDescription("The account id for the Cloudflare account."), + field.WithRequired(true), ) - EmailIdField = field.StringField( + emailIdField = field.StringField( "email-id", + field.WithDisplayName("Email ID"), field.WithDescription("The email id for the Cloudflare account."), + field.WithRequired(true), ) - - FieldRelationships = []field.SchemaFieldRelationship{ - field.FieldsAtLeastOneUsed(ApiTokenField, ApiKeyField), - field.FieldsDependentOn( - []field.SchemaField{ApiKeyField}, - []field.SchemaField{EmailIdField}, - ), + configurationFields = []field.SchemaField{ + apiKeyField, + apiTokenField, + accountIdField, + emailIdField, } ) //go:generate go run ./gen -var Config = field.NewConfiguration([]field.SchemaField{ - ApiKeyField, - ApiTokenField, - AccountIdField, - EmailIdField, -}, field.WithConstraints(FieldRelationships...)) - -func ValidateConfig(cfg *Cloudflare) error { - return nil -} +var Config = field.NewConfiguration( + configurationFields, + field.WithConnectorDisplayName("Cloudflare"), + field.WithHelpUrl("/docs/baton/cloudflare"), + field.WithIconUrl("/static/app-icons/cloudflare.svg"), + field.WithFieldGroups([]field.SchemaFieldGroup{ + { + Name: "api-token-group", + DisplayName: "API Token", + HelpText: "Use an API token for authentication.", + Fields: []field.SchemaField{accountIdField, apiTokenField}, + }, + { + Name: "api-key-group", + DisplayName: "Email + API key", + HelpText: "Use an API key with email for authentication.", + Fields: []field.SchemaField{accountIdField, emailIdField, apiKeyField}, + Default: true, + }, + }), +) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index e8809e7b..12a5840d 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -6,39 +6,41 @@ import ( "io" "github.com/cloudflare/cloudflare-go" + cfg "github.com/conductorone/baton-cloudflare/pkg/config" "github.com/conductorone/baton-sdk/pkg/annotations" + "github.com/conductorone/baton-sdk/pkg/cli" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/uhttp" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" ) -func New(ctx context.Context, cfg Config) (*Cloudflare, error) { +func New(ctx context.Context, cc *cfg.Cloudflare, opts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) { var ( client *cloudflare.API - apiKey = cfg.ApiKey - apiToken = cfg.ApiToken - accountId = cfg.AccountId - emailId = cfg.EmailId + apiKey = cc.ApiKey + apiToken = cc.ApiToken + accountId = cc.AccountId + emailId = cc.EmailId err error ) httpClient, err := uhttp.NewClient(ctx, uhttp.WithLogger(true, nil)) if err != nil { - return nil, err + return nil, nil, err } if apiToken != "" { client, err = cloudflare.NewWithAPIToken(apiToken, cloudflare.HTTPClient(httpClient)) if err != nil { - return nil, err + return nil, nil, err } } if apiKey != "" && emailId != "" { client, err = cloudflare.New(apiKey, emailId, cloudflare.HTTPClient(httpClient)) if err != nil { - return nil, err + return nil, nil, err } } @@ -46,7 +48,7 @@ func New(ctx context.Context, cfg Config) (*Cloudflare, error) { client: client, accountId: accountId, emailId: emailId, - }, nil + }, nil, nil } func (c *Cloudflare) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) { @@ -85,8 +87,8 @@ func (c *Cloudflare) Asset(ctx context.Context, asset *v2.AssetRef) (string, io. return "", nil, nil } -func (c *Cloudflare) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncer { - return []connectorbuilder.ResourceSyncer{ +func (c *Cloudflare) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncerV2 { + return []connectorbuilder.ResourceSyncerV2{ userBuilder(c.client, c.accountId), roleBuilder(c.client, c.accountId, c.emailId), } diff --git a/pkg/connector/model.go b/pkg/connector/model.go index 2fa2908b..3710375b 100644 --- a/pkg/connector/model.go +++ b/pkg/connector/model.go @@ -2,13 +2,6 @@ package connector import "github.com/cloudflare/cloudflare-go" -type Config struct { - AccountId string - ApiToken string - EmailId string - ApiKey string -} - type Cloudflare struct { client *cloudflare.API accountId string diff --git a/pkg/connector/role.go b/pkg/connector/role.go index 505107a0..83f1bee8 100644 --- a/pkg/connector/role.go +++ b/pkg/connector/role.go @@ -12,7 +12,6 @@ import ( "github.com/cloudflare/cloudflare-go" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" ent "github.com/conductorone/baton-sdk/pkg/types/entitlement" "github.com/conductorone/baton-sdk/pkg/types/grant" rs "github.com/conductorone/baton-sdk/pkg/types/resource" @@ -70,18 +69,18 @@ func roleResource(role cloudflare.AccountRole, resourceTypeRole *v2.ResourceType return ret, nil } -func (o *roleResourceType) List(ctx context.Context, _ *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (o *roleResourceType) List(ctx context.Context, _ *v2.ResourceId, opts rs.SyncOpAttrs) ([]*v2.Resource, *rs.SyncOpResults, error) { // Empty params causes ListAccountRoles to auto paginate and return all account roles params := cloudflare.ListAccountRolesParams{} roles, err := o.client.ListAccountRoles(ctx, cloudflare.AccountIdentifier(o.accountId), params) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv := make([]*v2.Resource, 0, len(roles)) for _, role := range roles { roleResource, err := roleResource(role, resourceTypeRole, nil) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, roleResource) } @@ -91,14 +90,14 @@ func (o *roleResourceType) List(ctx context.Context, _ *v2.ResourceId, pt *pagin Name: "Super Administrator - All Privileges", }, resourceTypeRole, nil) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, adminRoleResource) - return rv, "", nil, nil + return rv, &rs.SyncOpResults{}, nil } -func (r *roleResourceType) Entitlements(ctx context.Context, resource *v2.Resource, token *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (r *roleResourceType) Entitlements(ctx context.Context, resource *v2.Resource, opts rs.SyncOpAttrs) ([]*v2.Entitlement, *rs.SyncOpResults, error) { rv := []*v2.Entitlement{ ent.NewAssignmentEntitlement( resource, @@ -113,7 +112,7 @@ func (r *roleResourceType) Entitlements(ctx context.Context, resource *v2.Resour ), } - return rv, "", nil, nil + return rv, &rs.SyncOpResults{}, nil } // GetAccountMember returns an account member. @@ -163,17 +162,17 @@ func (r *roleResourceType) GetAccountMember(ctx context.Context, accountID strin return accountMemberListResponse, err } -func (r *roleResourceType) Grants(ctx context.Context, resource *v2.Resource, pt *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { +func (r *roleResourceType) Grants(ctx context.Context, resource *v2.Resource, opts rs.SyncOpAttrs) ([]*v2.Grant, *rs.SyncOpResults, error) { var rv []*v2.Grant - page, err := convertPageToken(pt.Token) + page, err := convertPageToken(opts.PageToken.Token) if err != nil { - return nil, "", nil, fmt.Errorf("Cloudflare: invalid page token error") + return nil, nil, fmt.Errorf("Cloudflare: invalid page token error") } pageOpts := cloudflare.PaginationOptions{Page: page} users, resp, err := r.client.AccountMembers(ctx, r.accountId, pageOpts) if err != nil { - return nil, "", nil, err + return nil, nil, err } roleId := resource.Id.Resource @@ -196,7 +195,7 @@ func (r *roleResourceType) Grants(ctx context.Context, resource *v2.Resource, pt } ur, err := userResource(accUser) if err != nil { - return nil, "", nil, wrapError(err, "failed to create user resource") + return nil, nil, wrapError(err, "failed to create user resource") } gr := grant.NewGrant(resource, roleMemberEntitlement, ur.Id) @@ -209,7 +208,7 @@ func (r *roleResourceType) Grants(ctx context.Context, resource *v2.Resource, pt rv = append(rv, gr) } - return rv, nextPage, nil, nil + return rv, &rs.SyncOpResults{NextPageToken: nextPage}, nil } func (r *roleResourceType) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) { diff --git a/pkg/connector/user.go b/pkg/connector/user.go index 474125d9..b59be019 100644 --- a/pkg/connector/user.go +++ b/pkg/connector/user.go @@ -6,8 +6,6 @@ import ( "github.com/cloudflare/cloudflare-go" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" rs "github.com/conductorone/baton-sdk/pkg/types/resource" ) @@ -55,16 +53,16 @@ func userResource(member cloudflare.AccountMember) (*v2.Resource, error) { return resource, nil } -func (o *UserResourceType) List(ctx context.Context, _ *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { - page, err := convertPageToken(pt.Token) +func (o *UserResourceType) List(ctx context.Context, _ *v2.ResourceId, opts rs.SyncOpAttrs) ([]*v2.Resource, *rs.SyncOpResults, error) { + page, err := convertPageToken(opts.PageToken.Token) if err != nil { - return nil, "", nil, fmt.Errorf("Cloudflare: invalid page token error") + return nil, nil, fmt.Errorf("Cloudflare: invalid page token error") } pageOpts := cloudflare.PaginationOptions{Page: page} users, resp, err := o.client.AccountMembers(ctx, o.accountId, pageOpts) if err != nil { - return nil, "", nil, fmt.Errorf("cloudflare: could not retrieve users: %w", err) + return nil, nil, fmt.Errorf("cloudflare: could not retrieve users: %w", err) } nextPage := convertNextPageToken(resp.Page, len(users)) @@ -72,20 +70,20 @@ func (o *UserResourceType) List(ctx context.Context, _ *v2.ResourceId, pt *pagin for _, user := range users { userResource, err := userResource(user) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, userResource) } - return rv, nextPage, nil, nil + return rv, &rs.SyncOpResults{NextPageToken: nextPage}, nil } -func (o *UserResourceType) Entitlements(_ context.Context, _ *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - return nil, "", nil, nil +func (o *UserResourceType) Entitlements(_ context.Context, _ *v2.Resource, _ rs.SyncOpAttrs) ([]*v2.Entitlement, *rs.SyncOpResults, error) { + return nil, nil, nil } -func (o *UserResourceType) Grants(_ context.Context, _ *v2.Resource, _ *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - return nil, "", nil, nil +func (o *UserResourceType) Grants(_ context.Context, _ *v2.Resource, _ rs.SyncOpAttrs) ([]*v2.Grant, *rs.SyncOpResults, error) { + return nil, nil, nil } func userBuilder(client *cloudflare.API, accountId string) *UserResourceType {