Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
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:
go get -d -u ./...
go mod tidy -v
go mod vendor

.PHONY: add-dep
add-dep:
.PHONY: add-deps
add-deps:
go mod tidy -v
go mod vendor

Expand Down
38 changes: 0 additions & 38 deletions cmd/baton-cloudflare/config.go

This file was deleted.

55 changes: 0 additions & 55 deletions cmd/baton-cloudflare/config_test.go

This file was deleted.

56 changes: 6 additions & 50 deletions cmd/baton-cloudflare/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
64 changes: 40 additions & 24 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Comment on lines +8 to 39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four fields (api-key, api-token, account-id, email-id) are now marked WithRequired(true). This breaks the previous behavior where users could authenticate with either api-token alone or api-key + email-id.

The old code used field.FieldsAtLeastOneUsed(apiTokenField, apiKeyField) and field.FieldsDependentOn(...) to enforce this correctly. These constraints have been removed and replaced with field groups, but field groups are UI-only metadata — they don't enforce validation.

Recommended fix: Either re-add field.WithConstraints(...) alongside the field groups, or remove WithRequired(true) from apiKeyField, apiTokenField, and emailIdField and rely on the constraint relationships instead.

Comment on lines +8 to 39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four fields now have WithRequired(true), but the field.WithConstraints(...) with FieldsAtLeastOneUsed and FieldsDependentOn have been removed. Field groups are UI-only metadata — they don't enforce validation at runtime.

This means users must provide all of api-key, api-token, account-id, and email-id, breaking the prior either/or authentication model where they could use api-token alone or api-key + email-id.

The old config_test.go (now deleted) validated this constraint behavior with test cases like "with api token" (valid), "with api key but missing email ID" (invalid), etc.

Fix: Remove WithRequired(true) from apiKeyField, apiTokenField, and emailIdField, and re-add field.WithConstraints(...):

Suggested change
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,
}
apiKeyField = field.StringField(
"api-key",
field.WithDisplayName("API Key"),
field.WithDescription("The api key for the Cloudflare account."),
field.WithIsSecret(true),
)
apiTokenField = field.StringField(
"api-token",
field.WithDisplayName("API Token"),
field.WithDescription("The api token for the Cloudflare account."),
field.WithIsSecret(true),
)
accountIdField = field.StringField(
"account-id",
field.WithDisplayName("Account ID"),
field.WithDescription("The account id for the Cloudflare account."),
field.WithRequired(true),
)
emailIdField = field.StringField(
"email-id",
field.WithDisplayName("Email ID"),
field.WithDescription("The email id for the Cloudflare account."),
)
configurationFields = []field.SchemaField{
apiKeyField,
apiTokenField,
accountIdField,
emailIdField,
}

And add constraints back to the Config definition:

var Config = field.NewConfiguration(
	configurationFields,
	field.WithConnectorDisplayName("Cloudflare"),
	field.WithHelpUrl("/docs/baton/cloudflare"),
	field.WithIconUrl("/static/app-icons/cloudflare.svg"),
	field.WithConstraints(
		field.FieldsAtLeastOneUsed(apiTokenField, apiKeyField),
		field.FieldsDependentOn(
			[]field.SchemaField{emailIdField},
			[]field.SchemaField{apiKeyField},
		),
	),
	field.WithFieldGroups([]field.SchemaFieldGroup{...}),
)

)

//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,
},
}),
)
24 changes: 13 additions & 11 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,49 @@ 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
}
}
Comment on lines 33 to 45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both if blocks can execute sequentially — if the user provides both apiToken and apiKey+emailId, the second block silently overwrites the token-based client. Use else if to make these mutually exclusive.

Also, if neither auth method matches (e.g., apiKey provided without emailId), client remains nil and will cause nil pointer panics during sync. Consider adding a guard after the auth blocks:

if client == nil {
    return nil, nil, fmt.Errorf("baton-cloudflare: either api-token or both api-key and email-id must be provided")
}


return &Cloudflare{
client: client,
accountId: accountId,
emailId: emailId,
}, nil
}, nil, nil
Comment on lines 33 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential nil client when API key is provided without email.

The configuration requires at least one of apiToken or apiKey via FieldsAtLeastOneUsed, but the emailIdField dependency may be configured incorrectly (see config.go comment). If a user provides apiKey without emailId:

  1. Line 33: apiToken != "" is false, client remains nil
  2. Line 40: apiKey != "" && emailId != "" is false (emailId is empty), client remains nil
  3. A nil client is returned, causing a delayed failure in Validate()

Consider adding early validation in New():

Proposed fix
+	if apiToken == "" && (apiKey == "" || emailId == "") {
+		return nil, nil, fmt.Errorf("cloudflare: either api-token or both api-key and email-id must be provided")
+	}
+
 	if apiToken != "" {
🤖 Prompt for AI Agents
In `@pkg/connector/connector.go` around lines 33 - 51, The New function can return
a nil client when apiKey is set but emailId is empty; update New() to validate
inputs early: if apiToken is empty and apiKey provided without emailId, return a
clear error (e.g., "apiKey requires emailId") instead of constructing a
Cloudflare with nil client, or more generally after attempting both
cloudflare.NewWithAPIToken and cloudflare.New check if client == nil and return
an error; reference the New() function, the Cloudflare struct, and the
apiToken/apiKey/emailId variables and ensure callers relying on Validate() get a
deterministic failure.

Comment on lines 33 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

  1. Both if blocks can execute sequentially. If the user provides both apiToken and apiKey+emailId, the second block silently overwrites the token-based client. Use else if to make these mutually exclusive.

  2. Nil client possible. If neither auth method matches (e.g., apiKey provided without emailId due to the now-broken config validation), client remains nil and will cause nil pointer panics during sync.

Suggested change
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
}
}
return &Cloudflare{
client: client,
accountId: accountId,
emailId: emailId,
}, nil
}, nil, nil
if apiToken != "" {
client, err = cloudflare.NewWithAPIToken(apiToken, cloudflare.HTTPClient(httpClient))
if err != nil {
return nil, nil, err
}
} else if apiKey != "" && emailId != "" {
client, err = cloudflare.New(apiKey, emailId, cloudflare.HTTPClient(httpClient))
if err != nil {
return nil, nil, err
}
}
if client == nil {
return nil, nil, fmt.Errorf("baton-cloudflare: either api-token or both api-key and email-id must be provided")
}
return &Cloudflare{
client: client,
accountId: accountId,
emailId: emailId,
}, nil, nil

}

func (c *Cloudflare) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
Expand Down Expand Up @@ -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),
}
Expand Down
7 changes: 0 additions & 7 deletions pkg/connector/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading