diff --git a/cmd/baton-ldap/config.go b/cmd/baton-ldap/config.go index af832ec..36cdd1f 100644 --- a/cmd/baton-ldap/config.go +++ b/cmd/baton-ldap/config.go @@ -35,22 +35,16 @@ var configurationFields = []field.SchemaField{ var configRelations = []field.SchemaFieldRelationship{ field.FieldsMutuallyExclusive(domainField, urlField), + field.FieldsAtLeastOneUsed(domainField, urlField), } +var configuration = field.NewConfiguration(configurationFields, configRelations...) + // validateConfig is run after the configuration is loaded, and should return an error if it isn't valid. func validateConfig(ctx context.Context, v *viper.Viper) error { l := ctxzap.Extract(ctx) - domain := v.GetString(domainField.FieldName) urlstr := v.GetString(urlField.FieldName) - if domain == "" && urlstr == "" { - return fmt.Errorf("domain or url is required") - } - - if domain != "" && urlstr != "" { - return fmt.Errorf("only one of domain or url is allowed") - } - if urlstr != "" { _, err := url.Parse(urlstr) if err != nil { diff --git a/cmd/baton-ldap/main.go b/cmd/baton-ldap/main.go index 03b82f0..1de4cb8 100644 --- a/cmd/baton-ldap/main.go +++ b/cmd/baton-ldap/main.go @@ -19,7 +19,7 @@ var version = "dev" func main() { ctx := context.Background() - _, cmd, err := configschema.DefineConfiguration(ctx, "baton-ldap", getConnector, configurationFields, configRelations) + _, cmd, err := configschema.DefineConfiguration(ctx, "baton-ldap", getConnector, configuration) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) diff --git a/go.mod b/go.mod index 2dd38ad..af410ea 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.22.3 require ( - github.com/conductorone/baton-sdk v0.2.1 + github.com/conductorone/baton-sdk v0.2.7 github.com/go-ldap/ldap/v3 v3.4.5 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jackc/puddle/v2 v2.2.1 diff --git a/go.sum b/go.sum index e98bf1d..9bc0395 100644 --- a/go.sum +++ b/go.sum @@ -54,10 +54,8 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/conductorone/baton-sdk v0.2.0 h1:aynxpdTHok/MTiHBWqWV32ptmMWUky3X7NEcG76ipMM= -github.com/conductorone/baton-sdk v0.2.0/go.mod h1:cg5FyUcJnD7xK5SPbHe/KNpwUVVlpHJ9rnmd3UwxSkU= -github.com/conductorone/baton-sdk v0.2.1 h1:Ft46eoVFO3q3Op/G65dcqubZe2pw/44GcpD+KnaVyq8= -github.com/conductorone/baton-sdk v0.2.1/go.mod h1:cg5FyUcJnD7xK5SPbHe/KNpwUVVlpHJ9rnmd3UwxSkU= +github.com/conductorone/baton-sdk v0.2.7 h1:mzp7H0zVeZLMvdGj3Yx31mpzM6ytNKI6QmpzRuiPlVE= +github.com/conductorone/baton-sdk v0.2.7/go.mod h1:cg5FyUcJnD7xK5SPbHe/KNpwUVVlpHJ9rnmd3UwxSkU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go new file mode 100644 index 0000000..445884a --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: c1/connector/v2/annotation_external_ticket.proto + +package v2 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ExternalTicketSettings struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *ExternalTicketSettings) Reset() { + *x = ExternalTicketSettings{} + if protoimpl.UnsafeEnabled { + mi := &file_c1_connector_v2_annotation_external_ticket_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExternalTicketSettings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExternalTicketSettings) ProtoMessage() {} + +func (x *ExternalTicketSettings) ProtoReflect() protoreflect.Message { + mi := &file_c1_connector_v2_annotation_external_ticket_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExternalTicketSettings.ProtoReflect.Descriptor instead. +func (*ExternalTicketSettings) Descriptor() ([]byte, []int) { + return file_c1_connector_v2_annotation_external_ticket_proto_rawDescGZIP(), []int{0} +} + +func (x *ExternalTicketSettings) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +var File_c1_connector_v2_annotation_external_ticket_proto protoreflect.FileDescriptor + +var file_c1_connector_v2_annotation_external_ticket_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, + 0x32, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x78, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x32, 0x22, 0x32, 0x0a, 0x16, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x54, + 0x69, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x6f, + 0x6e, 0x65, 0x2f, 0x62, 0x61, 0x74, 0x6f, 0x6e, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x2f, + 0x63, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x32, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_c1_connector_v2_annotation_external_ticket_proto_rawDescOnce sync.Once + file_c1_connector_v2_annotation_external_ticket_proto_rawDescData = file_c1_connector_v2_annotation_external_ticket_proto_rawDesc +) + +func file_c1_connector_v2_annotation_external_ticket_proto_rawDescGZIP() []byte { + file_c1_connector_v2_annotation_external_ticket_proto_rawDescOnce.Do(func() { + file_c1_connector_v2_annotation_external_ticket_proto_rawDescData = protoimpl.X.CompressGZIP(file_c1_connector_v2_annotation_external_ticket_proto_rawDescData) + }) + return file_c1_connector_v2_annotation_external_ticket_proto_rawDescData +} + +var file_c1_connector_v2_annotation_external_ticket_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_c1_connector_v2_annotation_external_ticket_proto_goTypes = []interface{}{ + (*ExternalTicketSettings)(nil), // 0: c1.connector.v2.ExternalTicketSettings +} +var file_c1_connector_v2_annotation_external_ticket_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_c1_connector_v2_annotation_external_ticket_proto_init() } +func file_c1_connector_v2_annotation_external_ticket_proto_init() { + if File_c1_connector_v2_annotation_external_ticket_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_c1_connector_v2_annotation_external_ticket_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExternalTicketSettings); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_c1_connector_v2_annotation_external_ticket_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_c1_connector_v2_annotation_external_ticket_proto_goTypes, + DependencyIndexes: file_c1_connector_v2_annotation_external_ticket_proto_depIdxs, + MessageInfos: file_c1_connector_v2_annotation_external_ticket_proto_msgTypes, + }.Build() + File_c1_connector_v2_annotation_external_ticket_proto = out.File + file_c1_connector_v2_annotation_external_ticket_proto_rawDesc = nil + file_c1_connector_v2_annotation_external_ticket_proto_goTypes = nil + file_c1_connector_v2_annotation_external_ticket_proto_depIdxs = nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go new file mode 100644 index 0000000..abe6a36 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_external_ticket.pb.validate.go @@ -0,0 +1,140 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: c1/connector/v2/annotation_external_ticket.proto + +package v2 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on ExternalTicketSettings with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *ExternalTicketSettings) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ExternalTicketSettings with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ExternalTicketSettingsMultiError, or nil if none found. +func (m *ExternalTicketSettings) ValidateAll() error { + return m.validate(true) +} + +func (m *ExternalTicketSettings) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Enabled + + if len(errors) > 0 { + return ExternalTicketSettingsMultiError(errors) + } + + return nil +} + +// ExternalTicketSettingsMultiError is an error wrapping multiple validation +// errors returned by ExternalTicketSettings.ValidateAll() if the designated +// constraints aren't met. +type ExternalTicketSettingsMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ExternalTicketSettingsMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ExternalTicketSettingsMultiError) AllErrors() []error { return m } + +// ExternalTicketSettingsValidationError is the validation error returned by +// ExternalTicketSettings.Validate if the designated constraints aren't met. +type ExternalTicketSettingsValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ExternalTicketSettingsValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ExternalTicketSettingsValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ExternalTicketSettingsValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ExternalTicketSettingsValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ExternalTicketSettingsValidationError) ErrorName() string { + return "ExternalTicketSettingsValidationError" +} + +// Error satisfies the builtin error interface +func (e ExternalTicketSettingsValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sExternalTicketSettings.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ExternalTicketSettingsValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ExternalTicketSettingsValidationError{} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go b/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go index e7cf616..34edb0c 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go @@ -11,6 +11,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" v1 "github.com/conductorone/baton-sdk/pb/c1/connector_wrapper/v1" "github.com/conductorone/baton-sdk/pkg/connectorrunner" + "github.com/conductorone/baton-sdk/pkg/field" "github.com/conductorone/baton-sdk/pkg/logging" "github.com/conductorone/baton-sdk/pkg/types" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" @@ -28,10 +29,16 @@ func MakeMainCommand( ctx context.Context, name string, v *viper.Viper, + confschema field.Configuration, getconnector GetConnectorFunc, opts ...connectorrunner.Option, ) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { + // validate required fields and relationship constraints + if err := field.Validate(confschema, v); err != nil { + return err + } + runCtx, err := initLogger( ctx, name, @@ -162,9 +169,15 @@ func MakeGRPCServerCommand( ctx context.Context, name string, v *viper.Viper, + confschema field.Configuration, getconnector GetConnectorFunc, ) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { + // validate required fields and relationship constraints + if err := field.Validate(confschema, v); err != nil { + return err + } + runCtx, err := initLogger( ctx, name, diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go b/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go index 0121851..6f59a46 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/config/config.go @@ -13,6 +13,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/connectorrunner" "github.com/conductorone/baton-sdk/pkg/field" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -20,8 +21,7 @@ func DefineConfiguration( ctx context.Context, connectorName string, connector cli.GetConnectorFunc, - fields []field.SchemaField, - constrains []field.SchemaFieldRelationship, + schema field.Configuration, options ...connectorrunner.Option, ) (*viper.Viper, *cobra.Command, error) { v := viper.New() @@ -44,8 +44,8 @@ func DefineConfiguration( v.AutomaticEnv() // add default fields and constrains - fields = field.EnsureDefaultFieldsExists(fields) - constrains = field.EnsureDefaultRelationships(constrains) + schema.Fields = field.EnsureDefaultFieldsExists(schema.Fields) + schema.Constraints = field.EnsureDefaultRelationships(schema.Constraints) // setup CLI with cobra mainCMD := &cobra.Command{ @@ -53,11 +53,11 @@ func DefineConfiguration( Short: connectorName, SilenceErrors: true, SilenceUsage: true, - RunE: cli.MakeMainCommand(ctx, connectorName, v, connector, options...), + RunE: cli.MakeMainCommand(ctx, connectorName, v, schema, connector, options...), } // add options to the main command - for _, field := range fields { + for _, field := range schema.Fields { switch field.FieldType { case reflect.Bool: value, err := field.Bool() @@ -95,6 +95,18 @@ func DefineConfiguration( } mainCMD.PersistentFlags(). StringP(field.FieldName, field.CLIShortHand, value, field.GetDescription()) + case reflect.Slice: + value, err := field.StringArray() + if err != nil { + return nil, nil, fmt.Errorf( + "field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + mainCMD.PersistentFlags(). + StringArrayP(field.FieldName, field.CLIShortHand, value, field.GetDescription()) default: return nil, nil, fmt.Errorf( "field %s, %s is not yet supported", @@ -115,15 +127,34 @@ func DefineConfiguration( ) } } + + // mark required + if field.Required { + if field.FieldType == reflect.Bool { + return nil, nil, fmt.Errorf("requiring %s of type %s does not make sense", field.FieldName, field.FieldType) + } + + err := mainCMD.MarkPersistentFlagRequired(field.FieldName) + if err != nil { + return nil, nil, fmt.Errorf( + "cannot require field %s, %s: %w", + field.FieldName, + field.FieldType, + err, + ) + } + } } // apply constrains - for _, constrain := range constrains { + for _, constrain := range schema.Constraints { switch constrain.Kind { case field.MutuallyExclusive: mainCMD.MarkFlagsMutuallyExclusive(listFieldConstrainsAsStrings(constrain)...) case field.RequiredTogether: mainCMD.MarkFlagsRequiredTogether(listFieldConstrainsAsStrings(constrain)...) + case field.AtLeastOne: + mainCMD.MarkFlagsOneRequired(listFieldConstrainsAsStrings(constrain)...) } } @@ -138,7 +169,7 @@ func DefineConfiguration( Use: "_connector-service", Short: "Start the connector service", Hidden: true, - RunE: cli.MakeGRPCServerCommand(ctx, connectorName, v, connector), + RunE: cli.MakeGRPCServerCommand(ctx, connectorName, v, schema, connector), } mainCMD.AddCommand(grpcServerCmd) @@ -149,7 +180,14 @@ func DefineConfiguration( } mainCMD.AddCommand(capabilitiesCmd) - mainCMD.AddCommand(cli.AdditionalCommands(name, fields)...) + mainCMD.AddCommand(cli.AdditionalCommands(name, schema.Fields)...) + + // NOTE (shackra): we don't check subcommands (i.e.: grpcServerCmd and capabilitiesCmd) + mainCMD.PersistentFlags().VisitAll(func(f *pflag.Flag) { + if v.IsSet(f.Name) { + _ = mainCMD.Flags().Set(f.Name, v.GetString(f.Name)) + } + }) return v, mainCMD, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/config/validation.go b/vendor/github.com/conductorone/baton-sdk/pkg/config/validation.go deleted file mode 100644 index 940f220..0000000 --- a/vendor/github.com/conductorone/baton-sdk/pkg/config/validation.go +++ /dev/null @@ -1,64 +0,0 @@ -package config - -import ( - "fmt" - "reflect" - "strings" - - "github.com/conductorone/baton-sdk/pkg/field" - "github.com/spf13/viper" -) - -type ConfigurationError struct { - errs []error -} - -func (c *ConfigurationError) Error() string { - amount := len(c.errs) - var errstrings []string - for _, err := range c.errs { - errstrings = append(errstrings, err.Error()) - } - - return fmt.Sprintf("found %d error(s) in the configuration:\n%s", amount, strings.Join(errstrings, "\n")) -} - -func (c *ConfigurationError) PushError(err error) { - c.errs = append(c.errs, err) -} - -// ValidateConfiguration checks if fields marked as required have a non -// zero-value set either from the CLI or from the configuration file. -func ValidateConfiguration(v *viper.Viper, fields []field.SchemaField) error { - errorsFound := &ConfigurationError{} - - for _, field := range fields { - var fieldError error - switch field.FieldType { - case reflect.Bool: - value := v.GetBool(field.FieldName) - if !value && field.Required { - fieldError = fmt.Errorf("field %s of type %s is marked as required, but no value was provided (value %t)", field.FieldName, field.FieldType, value) - } - case reflect.Int: - value := v.GetInt(field.FieldName) - if value == 0 && field.Required { - fieldError = fmt.Errorf("field %s of type %s is marked as required, but no value was provided (value %d)", field.FieldName, field.FieldType, value) - } - case reflect.String: - value := v.GetString(field.FieldName) - if value == "" && field.Required { - fieldError = fmt.Errorf("field %s of type %s is marked as required, but no value was provided (value '%s')", field.FieldName, field.FieldType, value) - } - default: - fieldError = fmt.Errorf("field %s has unsupported type %s ", field.FieldName, field.FieldType) - } - errorsFound.PushError(fieldError) - } - - if len(errorsFound.errs) > 0 { - return errorsFound - } - - return nil -} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go b/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go index 4f34a09..48616d4 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/connectorbuilder/connectorbuilder.go @@ -2,6 +2,7 @@ package connectorbuilder import ( "context" + "errors" "fmt" "sort" "time" @@ -86,6 +87,7 @@ type builderImpl struct { eventFeed EventProvider cb ConnectorBuilder ticketManager TicketManager + ticketingEnabled bool m *metrics.M nowFunc func() time.Time } @@ -290,6 +292,16 @@ func NewConnector(ctx context.Context, in interface{}, opts ...Opt) (types.Conne type Opt func(b *builderImpl) error +func WithTicketingEnabled() Opt { + return func(b *builderImpl) error { + if _, ok := b.cb.(TicketManager); ok { + b.ticketingEnabled = true + return nil + } + return errors.New("external ticketing not supported") + } +} + func WithMetricsHandler(h metrics.Handler) Opt { return func(b *builderImpl) error { b.m = metrics.New(h) @@ -442,6 +454,12 @@ func (b *builderImpl) GetMetadata(ctx context.Context, request *v2.ConnectorServ md.Capabilities = getCapabilities(ctx, b) + annos := annotations.Annotations(md.Annotations) + if b.ticketManager != nil { + annos.Append(&v2.ExternalTicketSettings{Enabled: b.ticketingEnabled}) + } + md.Annotations = annos + b.m.RecordTaskSuccess(ctx, tt, b.nowFunc().Sub(start)) return &v2.ConnectorServiceGetMetadataResponse{Metadata: md}, nil } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go index 4ea0509..7b08deb 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go @@ -51,12 +51,29 @@ func (s SchemaField) String() (string, error) { return value, nil } +// StringArray retuns the default value as a string array. +func (s SchemaField) StringArray() ([]string, error) { + value, ok := s.DefaultValue.([]string) + if !ok { + return nil, WrongValueTypeErr + } + + return value, nil +} + func (s SchemaField) GetDescription() string { + var line string if s.Description == "" { - return fmt.Sprintf("($BATON_%s)", toUpperCase(s.FieldName)) + line = fmt.Sprintf("($BATON_%s)", toUpperCase(s.FieldName)) + } else { + line = fmt.Sprintf("%s ($BATON_%s)", s.Description, toUpperCase(s.FieldName)) } - return fmt.Sprintf("%s ($BATON_%s)", s.Description, toUpperCase(s.FieldName)) + if s.Required { + line = fmt.Sprintf("required: %s", line) + } + + return line } func (s SchemaField) GetName() string { @@ -78,6 +95,10 @@ func BoolField(name string, optional ...fieldOption) SchemaField { field = o(field) } + if field.Required { + panic(fmt.Sprintf("requiring %s of type %s does not make sense", field.FieldName, field.FieldType)) + } + return field } @@ -109,6 +130,20 @@ func IntField(name string, optional ...fieldOption) SchemaField { return field } +func StringArrayField(name string, optional ...fieldOption) SchemaField { + field := SchemaField{ + FieldName: name, + FieldType: reflect.Slice, + DefaultValue: []string{}, + } + + for _, o := range optional { + field = o(field) + } + + return field +} + func toUpperCase(i string) string { return strings.ReplaceAll(strings.ToUpper(i), "-", "_") } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go index 902a687..3ba0db7 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/relationships.go @@ -5,6 +5,7 @@ type Relationship int const ( RequiredTogether Relationship = iota + 1 MutuallyExclusive + AtLeastOne ) type SchemaFieldRelationship struct { @@ -25,3 +26,10 @@ func FieldsMutuallyExclusive(fields ...SchemaField) SchemaFieldRelationship { Fields: fields, } } + +func FieldsAtLeastOneUsed(fields ...SchemaField) SchemaFieldRelationship { + return SchemaFieldRelationship{ + Kind: AtLeastOne, + Fields: fields, + } +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go new file mode 100644 index 0000000..12ecf64 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/struct.go @@ -0,0 +1,13 @@ +package field + +type Configuration struct { + Fields []SchemaField + Constraints []SchemaFieldRelationship +} + +func NewConfiguration(fields []SchemaField, constraints ...SchemaFieldRelationship) Configuration { + return Configuration{ + Fields: fields, + Constraints: constraints, + } +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go new file mode 100644 index 0000000..244969f --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/validation.go @@ -0,0 +1,124 @@ +package field + +import ( + "fmt" + "reflect" + "strings" + + "github.com/spf13/viper" +) + +type ErrConfigurationMissingFields struct { + errors []error +} + +func (e *ErrConfigurationMissingFields) Error() string { + var messages []string + + for _, err := range e.errors { + messages = append(messages, err.Error()) + } + + return fmt.Sprintf("errors found:\n%s", strings.Join(messages, "\n")) +} + +func (e *ErrConfigurationMissingFields) Push(err error) { + e.errors = append(e.errors, err) +} + +// Validate perform validation of field requirement and constraints +// relationships after the configuration is read. +// We don't check the following: +// - if required fields are mutually exclusive +// - repeated fields (by name) are defined +// - if sets of fields are mutually exclusive and required +// together at the same time +func Validate(c Configuration, v *viper.Viper) error { + present := make(map[string]int) + missingFieldsError := &ErrConfigurationMissingFields{} + + // check if required fields are present + for _, f := range c.Fields { + isNonZero := false + switch f.FieldType { + case reflect.Bool: + isNonZero = v.GetBool(f.FieldName) + case reflect.Int: + isNonZero = v.GetInt(f.FieldName) != 0 + case reflect.String: + isNonZero = v.GetString(f.FieldName) != "" + case reflect.Slice: + isNonZero = len(v.GetStringSlice(f.FieldName)) == 0 + default: + return fmt.Errorf("field %s has unsupported type %s", f.FieldName, f.FieldType) + } + + if isNonZero { + present[f.FieldName] = 1 + } + + if f.Required && !isNonZero { + missingFieldsError.Push(fmt.Errorf("field %s of type %s is marked as required but it has a zero-value", f.FieldName, f.FieldType)) + } + } + + if len(missingFieldsError.errors) > 0 { + return missingFieldsError + } + + // check constraints + return validateConstraints(present, c.Constraints) +} + +func validateConstraints(fieldsPresent map[string]int, relationships []SchemaFieldRelationship) error { + for _, relationship := range relationships { + var present int + for _, f := range relationship.Fields { + present += fieldsPresent[f.FieldName] + } + if present > 1 && relationship.Kind == MutuallyExclusive { + return makeMutuallyExclusiveError(fieldsPresent, relationship) + } + if present > 0 && present < len(relationship.Fields) && relationship.Kind == RequiredTogether { + return makeNeededTogetherError(fieldsPresent, relationship) + } + if present == 0 && relationship.Kind == AtLeastOne { + return makeAtLeastOneError(fieldsPresent, relationship) + } + } + + return nil +} + +func makeMutuallyExclusiveError(fields map[string]int, relation SchemaFieldRelationship) error { + var found []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 1 { + found = append(found, f.FieldName) + } + } + + return fmt.Errorf("fields marked as mutually exclusive were set: %s", strings.Join(found, ", ")) +} + +func makeNeededTogetherError(fields map[string]int, relation SchemaFieldRelationship) error { + var found []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 0 { + found = append(found, f.FieldName) + } + } + + return fmt.Errorf("fields marked as needed together are missing: %s", strings.Join(found, ", ")) +} + +func makeAtLeastOneError(fields map[string]int, relation SchemaFieldRelationship) error { + var found []string + for _, f := range relation.Fields { + if fields[f.FieldName] == 0 { + found = append(found, f.FieldName) + } + } + + return fmt.Errorf("at least one field was expected, any of: %s", strings.Join(found, ", ")) +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go index e51c2c4..bea170c 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/cycle.go @@ -55,7 +55,10 @@ func (g *EntitlementGraph) removeNode(nodeID int) { // Delete from reverse mapping. if node, ok := g.Nodes[nodeID]; ok { for _, entitlementID := range node.EntitlementIDs { - delete(g.EntitlementsToNodes, entitlementID) + entNodeId, ok := g.EntitlementsToNodes[entitlementID] + if ok && entNodeId == nodeID { + delete(g.EntitlementsToNodes, entitlementID) + } } } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go index 7736b80..dbdce81 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/graph.go @@ -37,8 +37,8 @@ type Node struct { // This is because the graph can have cycles, and we address them by reducing // _all_ nodes in a cycle into a single node. type EntitlementGraph struct { - NextNodeID int `json:"node_count"` // Automatically incremented so that each node has a unique ID. - NextEdgeID int `json:"edge_count"` // Automatically incremented so that each edge has a unique ID. + NextNodeID int `json:"next_node_id"` // Automatically incremented so that each node has a unique ID. + NextEdgeID int `json:"next_edge_id"` // Automatically incremented so that each edge has a unique ID. Nodes map[int]Node `json:"nodes"` // The mapping of all node IDs to nodes. EntitlementsToNodes map[string]int `json:"entitlements_to_nodes"` // Internal mapping of entitlements to nodes for quicker lookup. SourcesToDestinations map[int]map[int]int `json:"sources_to_destinations"` // Internal mapping of outgoing edges by node ID. diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go index a234598..fdb94e8 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/expand/validate.go @@ -3,6 +3,7 @@ package expand import ( "errors" "fmt" + "slices" "strings" ) @@ -66,13 +67,21 @@ func (g *EntitlementGraph) Str() string { // validateEdges validates that for every edge, both nodes actually exists. func (g *EntitlementGraph) validateEdges() error { - for _, edge := range g.Edges { + for edgeId, edge := range g.Edges { if _, ok := g.Nodes[edge.SourceID]; !ok { return ErrNoEntitlement } if _, ok := g.Nodes[edge.DestinationID]; !ok { return ErrNoEntitlement } + + if g.SourcesToDestinations[edge.SourceID][edge.DestinationID] != edgeId { + return fmt.Errorf("edge %v does not match source %v to destination %v", edgeId, edge.SourceID, edge.DestinationID) + } + + if g.DestinationsToSources[edge.DestinationID][edge.SourceID] != edgeId { + return fmt.Errorf("edge %v does not match destination %v to source %v", edgeId, edge.DestinationID, edge.SourceID) + } } return nil } @@ -91,6 +100,23 @@ func (g *EntitlementGraph) validateNodes() error { return fmt.Errorf("entitlement %v is in multiple nodes: %v %v", entID, nodeID, seenEntitlements[entID]) } seenEntitlements[entID] = nodeID + entNodeId, ok := g.EntitlementsToNodes[entID] + if !ok { + return fmt.Errorf("entitlement %v is not in EntitlementsToNodes. should be in node %v", entID, nodeID) + } + if entNodeId != nodeID { + return fmt.Errorf("entitlement %v is in node %v but should be in node %v", entID, entNodeId, nodeID) + } + } + } + + for entID, nodeID := range g.EntitlementsToNodes { + node, ok := g.Nodes[nodeID] + if !ok { + return fmt.Errorf("entitlement %v is in EntitlementsToNodes but not in Nodes", entID) + } + if !slices.Contains(node.EntitlementIDs, entID) { + return fmt.Errorf("entitlement %v is in EntitlementsToNodes but not in node %v", entID, nodeID) } } return nil diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go b/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go index 198230a..fc4febc 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sync/syncer.go @@ -82,18 +82,27 @@ func (s *syncer) handleProgress(ctx context.Context, a *Action, c int) { } } +var attempts = 0 + func shouldWaitAndRetry(ctx context.Context, err error) bool { + if err == nil { + attempts = 0 + return true + } if status.Code(err) != codes.Unavailable { return false } + attempts++ l := ctxzap.Extract(ctx) - l.Error("retrying operation", zap.Error(err)) + + var wait time.Duration = time.Duration(attempts) * time.Second + + l.Error("retrying operation", zap.Error(err), zap.Duration("wait", wait)) for { select { - // TODO: this should back off based on error counts - case <-time.After(1 * time.Second): + case <-time.After(wait): return true case <-ctx.Done(): return false @@ -191,28 +200,28 @@ func (s *syncer) Sync(ctx context.Context) error { case SyncResourceTypesOp: err = s.SyncResourceTypes(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue case SyncResourcesOp: err = s.SyncResources(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue case SyncEntitlementsOp: err = s.SyncEntitlements(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue case SyncGrantsOp: err = s.SyncGrants(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue @@ -232,7 +241,7 @@ func (s *syncer) Sync(ctx context.Context) error { } err = s.SyncGrantExpansion(ctx) - if err != nil && !shouldWaitAndRetry(ctx, err) { + if !shouldWaitAndRetry(ctx, err) { return err } continue @@ -815,7 +824,11 @@ func (s *syncer) SyncGrantExpansion(ctx context.Context) error { if entitlementGraph.Loaded { cycle := entitlementGraph.GetFirstCycle() if cycle != nil { - l.Warn("cycle detected in entitlement graph", zap.Any("cycle", cycle)) + l.Warn( + "cycle detected in entitlement graph", + zap.Any("cycle", cycle), + zap.Any("initial graph", entitlementGraph), + ) if dontFixCycles { return fmt.Errorf("cycles detected in entitlement graph") } @@ -1302,7 +1315,11 @@ func (s *syncer) expandGrantsForEntitlements(ctx context.Context) error { } if graph.Depth > maxDepth { - l.Error("expandGrantsForEntitlements: exceeded max depth", zap.Any("graph", graph), zap.Int("max_depth", maxDepth)) + l.Error( + "expandGrantsForEntitlements: exceeded max depth", + zap.Any("graph", graph), + zap.Int("max_depth", maxDepth), + ) s.state.FinishAction(ctx) return fmt.Errorf("exceeded max depth") } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go b/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go index 6620ebb..8d251c6 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/types/ticket/custom_fields.go @@ -301,7 +301,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - if cf.Required && tv.StringValue.Value == "" { + if cf.Required && tv.StringValue.GetValue() == "" { l.Debug("error: invalid ticket: string value is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -313,7 +313,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - if cf.Required && len(tv.StringValues.Values) == 0 { + if cf.Required && len(tv.StringValues.GetValues()) == 0 { l.Debug("error: invalid ticket: string values is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -332,7 +332,7 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic return false, nil } - if cf.Required && tv.TimestampValue.Value == nil { + if cf.Required && tv.TimestampValue.GetValue() == nil { l.Debug("error: invalid ticket: expected timestamp value for field but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -347,7 +347,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValue := tv.PickStringValue.GetValue() allowedValues := v.PickStringValue.GetAllowedValues() - if cf.Required && ticketValue == "" { + // String value is empty but custom field is not required, skip further validation + if !cf.Required && ticketValue == "" { + continue + } + + // Custom field is required, check if string is empty + if ticketValue == "" { l.Debug("error: invalid ticket: expected string value for field but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -384,7 +390,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValues := tv.PickMultipleStringValues.GetValues() allowedValues := v.PickMultipleStringValues.GetAllowedValues() - if cf.Required && len(ticketValues) == 0 { + // String values are empty but custom field is not required, skip further validation + if !cf.Required && len(ticketValues) == 0 { + continue + } + + // Custom field is required so check if string values are empty + if len(ticketValues) == 0 { l.Debug("error: invalid ticket: string values is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -422,7 +434,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValue := tv.PickObjectValue.GetValue() allowedValues := v.PickObjectValue.GetAllowedValues() - if cf.Required && ticketValue == nil || ticketValue.GetId() == "" { + // Object value for field is nil, but custom field is not required, skip further validation + if !cf.Required && (ticketValue == nil || ticketValue.GetId() == "") { + continue + } + + // Custom field is required so check if object value for field is nil + if ticketValue == nil || ticketValue.GetId() == "" { l.Debug("error: invalid ticket: expected object value for field but was nil", zap.String("custom_field_id", cf.Id)) return false, nil } @@ -459,7 +477,13 @@ func ValidateTicket(ctx context.Context, schema *v2.TicketSchema, ticket *v2.Tic ticketValues := tv.PickMultipleObjectValues.GetValues() allowedValues := v.PickMultipleObjectValues.GetAllowedValues() - if cf.Required && len(ticketValues) == 0 { + // Object values are empty but custom field is not required, skip further validation + if !cf.Required && len(ticketValues) == 0 { + continue + } + + // Custom field is required so check if object values are empty + if len(ticketValues) == 0 { l.Debug("error: invalid ticket: object values is required but was empty", zap.String("custom_field_id", cf.Id)) return false, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 177e635..cf39416 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -143,7 +143,7 @@ github.com/aws/smithy-go/waiter # github.com/benbjohnson/clock v1.3.5 ## explicit; go 1.15 github.com/benbjohnson/clock -# github.com/conductorone/baton-sdk v0.2.1 +# github.com/conductorone/baton-sdk v0.2.7 ## explicit; go 1.21 github.com/conductorone/baton-sdk/internal/connector github.com/conductorone/baton-sdk/pb/c1/c1z/v1