diff --git a/go.mod b/go.mod index 0389f48e..cf5cb6d1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/conductorone/baton-github go 1.25.2 require ( - github.com/conductorone/baton-sdk v0.7.16 + github.com/conductorone/baton-sdk v0.7.19-0.20260209222658-3300146ac692 github.com/deckarep/golang-set/v2 v2.8.0 github.com/ennyjfrick/ruleguard-logfatal v0.0.2 github.com/golang-jwt/jwt/v5 v5.2.2 diff --git a/go.sum b/go.sum index 3fb4479f..75c21080 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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.7.16 h1:HPYpKJ1wmA4c/I8LlN9K7xDqQkCISFVPOEgZHf/O5mE= -github.com/conductorone/baton-sdk v0.7.16/go.mod h1:agmFrml6APUw4ZlqMEBrnXYj3aAOGKOJ6gztiNj64h0= +github.com/conductorone/baton-sdk v0.7.19-0.20260209222658-3300146ac692 h1:Ks/gROxKHGjB499wT4WFvNomseDMakoq73X+XO3MOHE= +github.com/conductorone/baton-sdk v0.7.19-0.20260209222658-3300146ac692/go.mod h1:agmFrml6APUw4ZlqMEBrnXYj3aAOGKOJ6gztiNj64h0= github.com/conductorone/dpop v0.2.3 h1:s91U3845GHQ6P6FWrdNr2SEOy1ES/jcFs1JtKSl2S+o= github.com/conductorone/dpop v0.2.3/go.mod h1:gyo8TtzB9SCFCsjsICH4IaLZ7y64CcrDXMOPBwfq/3s= github.com/conductorone/dpop/integrations/dpop_grpc v0.2.3 h1:kLMCNIh0Mo2vbvvkCmJ3ixsPbXEJ6HPcW53Ku9yje3s= diff --git a/pkg/config/conf.gen.go b/pkg/config/conf.gen.go index 4a963e2f..68687a95 100644 --- a/pkg/config/conf.gen.go +++ b/pkg/config/conf.gen.go @@ -1,18 +1,19 @@ // Code generated by baton-sdk. DO NOT EDIT!!! package config -import "reflect" +import ( + "reflect" +) type Github struct { - Token string `mapstructure:"token"` - Orgs []string `mapstructure:"orgs"` - Enterprises []string `mapstructure:"enterprises"` - InstanceUrl string `mapstructure:"instance-url"` - SyncSecrets bool `mapstructure:"sync-secrets"` - OmitArchivedRepositories bool `mapstructure:"omit-archived-repositories"` - AppId string `mapstructure:"app-id"` - AppPrivatekeyPath []byte `mapstructure:"app-privatekey-path"` - Org string `mapstructure:"org"` + Token string `mapstructure:"token"` + Orgs []string `mapstructure:"orgs"` + Enterprises []string `mapstructure:"enterprises"` + InstanceUrl string `mapstructure:"instance-url"` + SyncSecrets bool `mapstructure:"sync-secrets"` + OmitArchivedRepositories bool `mapstructure:"omit-archived-repositories"` + AppId string `mapstructure:"app-id"` + AppPrivatekeyPath string `mapstructure:"app-privatekey-path"` } func (c *Github) findFieldByTag(tagValue string) (any, bool) { @@ -54,6 +55,13 @@ func (c *Github) GetString(fieldName string) string { return string(t) } panic("wrong type") + if t, ok := v.(string); ok { + return t + } + if t, ok := v.([]byte); ok { + return string(t) + } + panic("wrong type") } func (c *Github) GetInt(fieldName string) int { diff --git a/pkg/connector/repository.go b/pkg/connector/repository.go index 28546fe9..598dda17 100644 --- a/pkg/connector/repository.go +++ b/pkg/connector/repository.go @@ -7,7 +7,9 @@ import ( "strconv" "strings" + config "github.com/conductorone/baton-sdk/pb/c1/config/v1" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/actions" "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" @@ -18,6 +20,7 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/structpb" ) // outside collaborators are given one of these roles too. @@ -439,3 +442,273 @@ func skipGrantsForResourceType(bag *pagination.Bag) (string, error) { } return pageToken, nil } + +// ResourceActions registers the resource actions for the repository resource type. +// This implements the ResourceActionProvider interface. +func (o *repositoryResourceType) ResourceActions(ctx context.Context, registry actions.ActionRegistry) error { + if err := o.registerCreateRepositoryAction(ctx, registry); err != nil { + return err + } + return nil +} + +func (o *repositoryResourceType) registerCreateRepositoryAction(ctx context.Context, registry actions.ActionRegistry) error { + return registry.Register(ctx, &v2.BatonActionSchema{ + Name: "create", + DisplayName: "Create Repository", + Description: "Create a new repository in a GitHub organization", + ActionType: []v2.ActionType{v2.ActionType_ACTION_TYPE_RESOURCE_CREATE}, + Constraints: []*config.Constraint{ + { + Kind: config.ConstraintKind_CONSTRAINT_KIND_DEPENDENT_ON, + FieldNames: []string{"license_template", "gitignore_template"}, + SecondaryFieldNames: []string{"add_readme"}, + Name: "README is required when license or gitignore template is selected", + HelpText: "The README.md file is required when a license or gitignore template is selected", + }, + }, + Arguments: []*config.Field{ + { + Name: "name", + DisplayName: "Repository name", + Description: "The name of the repository to create", + Field: &config.Field_StringField{}, + IsRequired: true, + }, + { + Name: "description", + DisplayName: "Description", + Description: "A description of the repository", + Field: &config.Field_StringField{}, + }, + { + Name: "org", + DisplayName: "Organization", + Description: "The organization to create the repository in", + Field: &config.Field_ResourceIdField{ + ResourceIdField: &config.ResourceIdField{ + Rules: &config.ResourceIDRules{ + AllowedResourceTypeIds: []string{resourceTypeOrg.Id}, + }, + }, + }, + IsRequired: true, + }, + { + Name: "visibility", + DisplayName: "Visibility", + Description: "The visibility level of the repository", + Field: &config.Field_StringField{ + StringField: &config.StringField{ + Options: []*config.StringFieldOption{ + {Value: "public", DisplayName: "Public", Name: "Anyone on the internet can view this repository"}, + {Value: "private", DisplayName: "Private", Name: "You can choose who can see this repository"}, + {Value: "internal", DisplayName: "Internal", Name: "Members of the enterprise can view this repository (enterprise only)"}, + }, + DefaultValue: "private", + }, + }, + }, + { + Name: "add_readme", + DisplayName: "Add README.md", + Description: "Add a README.md file to the repository", + Field: &config.Field_BoolField{ + BoolField: &config.BoolField{ + DefaultValue: true, + }, + }, + }, + { + Name: "gitignore_template", + DisplayName: "Gitignore Template", + Description: "Gitignore template to apply", + Field: &config.Field_StringField{ + StringField: &config.StringField{ + Options: []*config.StringFieldOption{ + {Value: "", DisplayName: "No .gitignore template"}, + {Value: "Go", DisplayName: "Go"}, + {Value: "Python", DisplayName: "Python"}, + {Value: "Node", DisplayName: "Node"}, + {Value: "Java", DisplayName: "Java"}, + {Value: "Ruby", DisplayName: "Ruby"}, + {Value: "Rust", DisplayName: "Rust"}, + {Value: "C++", DisplayName: "C++"}, + {Value: "C", DisplayName: "C"}, + {Value: "Swift", DisplayName: "Swift"}, + {Value: "Kotlin", DisplayName: "Kotlin"}, + {Value: "Scala", DisplayName: "Scala"}, + {Value: "Terraform", DisplayName: "Terraform"}, + }, + }, + }, + }, + { + Name: "license_template", + DisplayName: "License Template", + Description: "License template to apply", + Field: &config.Field_StringField{ + StringField: &config.StringField{ + Options: []*config.StringFieldOption{ + {Value: "", DisplayName: "No license"}, + {Value: "mit", DisplayName: "MIT License"}, + {Value: "apache-2.0", DisplayName: "Apache License 2.0"}, + {Value: "gpl-3.0", DisplayName: "GNU GPLv3"}, + {Value: "gpl-2.0", DisplayName: "GNU GPLv2"}, + {Value: "lgpl-3.0", DisplayName: "GNU LGPLv3"}, + {Value: "bsd-3-clause", DisplayName: "BSD 3-Clause"}, + {Value: "bsd-2-clause", DisplayName: "BSD 2-Clause"}, + {Value: "mpl-2.0", DisplayName: "Mozilla Public License 2.0"}, + {Value: "unlicense", DisplayName: "The Unlicense"}, + {Value: "agpl-3.0", DisplayName: "GNU AGPLv3"}, + }, + }, + }, + }, + }, + ReturnTypes: []*config.Field{ + {Name: "success", Field: &config.Field_BoolField{}}, + {Name: "resource", Field: &config.Field_ResourceField{}}, + {Name: "entitlements", DisplayName: "Entitlements", Field: &config.Field_EntitlementSliceField{ + EntitlementSliceField: &config.EntitlementSliceField{}, + }}, + {Name: "grants", DisplayName: "Grants", Field: &config.Field_GrantSliceField{ + GrantSliceField: &config.GrantSliceField{}, + }}, + }, + }, o.handleCreateRepositoryAction) +} + +func (o *repositoryResourceType) handleCreateRepositoryAction(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + + // Extract required arguments using SDK helpers + name, err := actions.RequireStringArg(args, "name") + if err != nil { + return nil, nil, err + } + + parentResourceID, err := actions.RequireResourceIDArg(args, "org") + if err != nil { + return nil, nil, err + } + + // Get the organization name from the parent resource ID + orgName, err := o.orgCache.GetOrgName(ctx, parentResourceID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get organization name: %w", err) + } + + l.Info("github-connector: creating repository via action", + zap.String("repo_name", name), + zap.String("org_name", orgName), + ) + + // Build the Repository request + newRepo := &github.Repository{ + Name: github.Ptr(name), + } + + // Extract optional fields using SDK helpers + if description, ok := actions.GetStringArg(args, "description"); ok && description != "" { + newRepo.Description = github.Ptr(description) + } + + if visibility, ok := actions.GetStringArg(args, "visibility"); ok && visibility != "" { + if visibility == "public" || visibility == "private" || visibility == "internal" { + newRepo.Visibility = github.Ptr(visibility) + } else { + return nil, nil, fmt.Errorf("invalid visibility: %q (must be \"public\", \"private\", or \"internal\")", visibility) + } + } + + // Extract template options first to validate AutoInit requirements + gitignoreTemplate, hasGitignore := actions.GetStringArg(args, "gitignore_template") + licenseTemplate, hasLicense := actions.GetStringArg(args, "license_template") + hasTemplates := (hasGitignore && gitignoreTemplate != "") || (hasLicense && licenseTemplate != "") + + // add_readme maps to AutoInit in GitHub API + // GitHub requires AutoInit=true when using gitignore_template or license_template + if addReadme, ok := actions.GetBoolArg(args, "add_readme"); ok { + if !addReadme && hasTemplates { + return nil, nil, fmt.Errorf("add_readme must be true when gitignore_template or license_template is provided (GitHub requires auto_init=true for templates)") + } + newRepo.AutoInit = github.Ptr(addReadme) + } else if hasTemplates { + // If templates are provided but add_readme wasn't explicitly set, enable AutoInit + newRepo.AutoInit = github.Ptr(true) + } + + if hasGitignore && gitignoreTemplate != "" { + newRepo.GitignoreTemplate = github.Ptr(gitignoreTemplate) + } + + if hasLicense && licenseTemplate != "" { + newRepo.LicenseTemplate = github.Ptr(licenseTemplate) + } + + // Create the repository via GitHub API + createdRepo, resp, err := o.client.Repositories.Create(ctx, orgName, newRepo) + if err != nil { + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to create repository %s in org %s", name, orgName)) + } + + // Extract rate limit data for annotations + var annos annotations.Annotations + if rateLimitData, err := extractRateLimitData(resp); err == nil { + annos.WithRateLimiting(rateLimitData) + } + + l.Info("github-connector: repository created successfully via action", + zap.String("repo_name", createdRepo.GetName()), + zap.Int64("repo_id", createdRepo.GetID()), + zap.String("repo_full_name", createdRepo.GetFullName()), + ) + + // Create the resource representation of the newly created repository + repoResource, err := repositoryResource(ctx, createdRepo, parentResourceID) + if err != nil { + return nil, annos, fmt.Errorf("failed to create resource representation: %w", err) + } + + // Generate entitlements for the newly created repository (reuse existing method) + entitlements, _, _, err := o.Entitlements(ctx, repoResource, nil) + if err != nil { + return nil, annos, fmt.Errorf("failed to generate entitlements: %w", err) + } + + // Fetch grants for the newly created repository by reusing the existing Grants method + var grants []*v2.Grant + pageToken := "" + for { + pToken := &pagination.Token{Token: pageToken} + pageGrants, nextToken, _, err := o.Grants(ctx, repoResource, pToken) + if err != nil { + l.Warn("github-connector: failed to fetch grants for repository", zap.Error(err)) + break + } + grants = append(grants, pageGrants...) + if nextToken == "" { + break + } + pageToken = nextToken + } + + // Build return values using SDK helpers + resourceRv, err := actions.NewResourceReturnField("resource", repoResource) + if err != nil { + return nil, annos, err + } + + entitlementsRv, err := actions.NewEntitlementListReturnField("entitlements", entitlements) + if err != nil { + return nil, annos, err + } + + grantsRv, err := actions.NewGrantListReturnField("grants", grants) + if err != nil { + return nil, annos, err + } + + return actions.NewReturnValues(true, resourceRv, entitlementsRv, grantsRv), annos, nil +} diff --git a/pkg/connector/repository_test.go b/pkg/connector/repository_test.go index e1d213e0..c569aa17 100644 --- a/pkg/connector/repository_test.go +++ b/pkg/connector/repository_test.go @@ -10,6 +10,7 @@ import ( resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" "github.com/conductorone/baton-github/test" "github.com/conductorone/baton-github/test/mocks" @@ -80,3 +81,171 @@ func TestRepository(t *testing.T) { require.Empty(t, revokeAnnotations) }) } + +func TestRepositoryActions(t *testing.T) { + ctx := context.Background() + + t.Run("should create a basic repository with name, description and optional fields", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + githubOrganization, _, _, _, _, _ := mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := repositoryBuilder(githubClient, cache, false) + + // Create args for the action + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-repo-basic", + "description": "A test repository for unit testing", + "visibility": "private", + "add_readme": true, + "gitignore_template": "Node", + "license_template": "apache-2.0", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", // Matches the seeded org ID + }, + }) + require.NoError(t, err) + + result, annos, err := client.handleCreateRepositoryAction(ctx, args) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, annos) + + // Verify success field + successVal := result.Fields["success"] + require.NotNil(t, successVal) + require.True(t, successVal.GetBoolValue()) + + // Verify resource was returned + resourceVal := result.Fields["resource"] + require.NotNil(t, resourceVal) + + _ = githubOrganization // Used in seed + }) + + t.Run("should create a public repository", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := repositoryBuilder(githubClient, cache, false) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-repo-public", + "description": "A public test repository", + "visibility": "public", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateRepositoryAction(ctx, args) + require.NoError(t, err) + require.NotNil(t, result) + require.True(t, result.Fields["success"].GetBoolValue()) + }) + + t.Run("should fail when templates are used but add_readme is false", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := repositoryBuilder(githubClient, cache, false) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-repo-template-no-readme", + "description": "This should fail", + "visibility": "private", + "add_readme": false, // Explicitly false with templates + "gitignore_template": "Python", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateRepositoryAction(ctx, args) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "add_readme must be true") + }) + + t.Run("should fail with missing required name field", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := repositoryBuilder(githubClient, cache, false) + + // Missing name field + args, err := structpb.NewStruct(map[string]interface{}{ + "description": "A repo without a name", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateRepositoryAction(ctx, args) + require.Error(t, err) + require.Nil(t, result) + }) + + t.Run("should fail with invalid visibility value", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := repositoryBuilder(githubClient, cache, false) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-repo-invalid-visibility", + "visibility": "invalid_visibility_value", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateRepositoryAction(ctx, args) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "invalid visibility") + }) + + t.Run("should create internal repository (enterprise feature)", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := repositoryBuilder(githubClient, cache, false) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-repo-internal", + "description": "An internal repository", + "visibility": "internal", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateRepositoryAction(ctx, args) + require.NoError(t, err) + require.NotNil(t, result) + require.True(t, result.Fields["success"].GetBoolValue()) + }) +} diff --git a/pkg/connector/team.go b/pkg/connector/team.go index e6eff4c7..fe2d5f10 100644 --- a/pkg/connector/team.go +++ b/pkg/connector/team.go @@ -6,7 +6,9 @@ import ( "strconv" "strings" + config "github.com/conductorone/baton-sdk/pb/c1/config/v1" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/actions" "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" @@ -17,11 +19,15 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/structpb" ) const ( teamRoleMember = "member" teamRoleMaintainer = "maintainer" + + teamPrivacySecret = "secret" + teamPrivacyClosed = "closed" ) var teamAccessLevels = []string{ @@ -368,6 +374,325 @@ func (o *teamResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotat return nil, nil } +// ResourceActions registers the resource actions for the team resource type. +// This implements the ResourceActionProvider interface. +func (o *teamResourceType) ResourceActions(ctx context.Context, registry actions.ActionRegistry) error { + if err := o.registerCreateTeamAction(ctx, registry); err != nil { + return err + } + return nil +} + +func (o *teamResourceType) registerCreateTeamAction(ctx context.Context, registry actions.ActionRegistry) error { + return registry.Register(ctx, &v2.BatonActionSchema{ + Name: "create", + DisplayName: "Create Team", + Description: "Create a new team in a GitHub organization", + ActionType: []v2.ActionType{v2.ActionType_ACTION_TYPE_RESOURCE_CREATE}, + Constraints: []*config.Constraint{ + { + Kind: config.ConstraintKind_CONSTRAINT_KIND_ALLOWED_OPTIONS, + FieldNames: []string{"privacy"}, + SecondaryFieldNames: []string{"parent"}, + AllowedOptionValues: []string{"closed"}, + Name: "Privacy must be closed if parent is set", + HelpText: "Privacy must be closed if parent is set", + }, + }, + Arguments: []*config.Field{ + { + Name: "name", + DisplayName: "Team name", + Description: "You'll use this name to mention this team in conversations.", + Field: &config.Field_StringField{}, + IsRequired: true, + }, + { + Name: "description", + DisplayName: "Description", + Description: "What is this team all about?", + Field: &config.Field_StringField{}, + }, + { + Name: "org", + DisplayName: "Organization", + Description: "The organization name.", + Field: &config.Field_ResourceIdField{ + ResourceIdField: &config.ResourceIdField{ + Rules: &config.ResourceIDRules{ + AllowedResourceTypeIds: []string{resourceTypeOrg.Id}, + }, + }, + }, + IsRequired: true, + }, + { + Name: "parent", + DisplayName: "Parent team", + Description: "The team to set as the parent team.", + Field: &config.Field_ResourceIdField{ + ResourceIdField: &config.ResourceIdField{ + Rules: &config.ResourceIDRules{ + AllowedResourceTypeIds: []string{resourceTypeTeam.Id}, + }, + }, + }, + }, + { + Name: "privacy", + DisplayName: "Privacy", + Description: "The level of privacy this team should have.", + Field: &config.Field_StringField{ + StringField: &config.StringField{ + Options: []*config.StringFieldOption{ + {Value: "secret", Name: "Secret is only visible to org owners and team members", DisplayName: "Secret"}, + {Value: "closed", Name: "Closed is visible to all org members. When parent team is set, this is the only allowed privacy level.", DisplayName: "Closed"}, + }, + DefaultValue: "closed", + }, + }, + }, + { + Name: "notifications_enabled", + DisplayName: "Team notifications", + Description: "When enabled, team members receive notifications when the team is @mentioned.", + Field: &config.Field_BoolField{ + BoolField: &config.BoolField{ + DefaultValue: true, + }, + }, + }, + { + Name: "maintainers", + DisplayName: "Team Maintainers", + Description: "List of user resource IDs for organization members who will become team maintainers.", + Field: &config.Field_ResourceIdSliceField{ + ResourceIdSliceField: &config.ResourceIdSliceField{ + Rules: &config.RepeatedResourceIdRules{ + AllowedResourceTypeIds: []string{resourceTypeUser.Id}, + }, + }, + }, + }, + { + Name: "repo_names", + DisplayName: "Repositories", + Description: "List of repository resource IDs to add the team to.", + Field: &config.Field_ResourceIdSliceField{ + ResourceIdSliceField: &config.ResourceIdSliceField{ + Rules: &config.RepeatedResourceIdRules{ + AllowedResourceTypeIds: []string{resourceTypeRepository.Id}, + }, + }, + }, + }, + }, + ReturnTypes: []*config.Field{ + {Name: "success", Field: &config.Field_BoolField{}}, + {Name: "resource", Field: &config.Field_ResourceField{}}, + {Name: "entitlements", DisplayName: "Entitlements", Field: &config.Field_EntitlementSliceField{ + EntitlementSliceField: &config.EntitlementSliceField{}, + }}, + {Name: "grants", DisplayName: "Grants", Field: &config.Field_GrantSliceField{ + GrantSliceField: &config.GrantSliceField{}, + }}, + }, + }, o.handleCreateTeamAction) +} + +func (o *teamResourceType) handleCreateTeamAction(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + + // Extract required arguments using SDK helpers + name, err := actions.RequireStringArg(args, "name") + if err != nil { + return nil, nil, err + } + + parentResourceID, err := actions.RequireResourceIDArg(args, "org") + if err != nil { + return nil, nil, err + } + + // Get the organization name from the parent resource ID + orgName, err := o.orgCache.GetOrgName(ctx, parentResourceID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get organization name: %w", err) + } + + l.Info("github-connector: creating team via action", + zap.String("team_name", name), + zap.String("org_name", orgName), + ) + + // Build the NewTeam request + newTeam := github.NewTeam{ + Name: name, + } + + // Extract optional fields using SDK helpers + if description, ok := actions.GetStringArg(args, "description"); ok && description != "" { + newTeam.Description = github.Ptr(description) + } + + // Check if this is a nested team (has parent) + isNestedTeam := false + if parentTeamResourceID, ok := actions.GetResourceIDArg(args, "parent"); ok { + parentTeamID, err := strconv.ParseInt(parentTeamResourceID.Resource, 10, 64) + if err != nil { + return nil, nil, fmt.Errorf("invalid parent team ID: %w", err) + } + + // Fetch the parent team to validate it's not a secret team + // GitHub does not allow child teams under secret parent teams + org, resp, err := o.client.Organizations.Get(ctx, orgName) + if err != nil { + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to get organization %s", orgName)) + } + parentTeam, resp, err := o.client.Teams.GetTeamByID(ctx, org.GetID(), parentTeamID) //nolint:staticcheck // TODO: migrate to GetTeamBySlug + if err != nil { + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to get parent team %d", parentTeamID)) + } + if parentTeam.GetPrivacy() == teamPrivacySecret { + return nil, nil, fmt.Errorf("cannot create child team: parent team %q has privacy set to \"secret\"; GitHub does not allow child teams under secret parent teams", parentTeam.GetName()) + } + + newTeam.ParentTeamID = github.Ptr(parentTeamID) + isNestedTeam = true + } + + // Handle privacy with constraints based on team type: + // - For non-nested teams: "secret" (default) or "closed" + // - For nested/child teams: only "closed" is allowed (default: closed) + if privacy, ok := actions.GetStringArg(args, "privacy"); ok && privacy != "" { + switch { + case isNestedTeam: + // Nested teams can only be "closed" + if privacy == teamPrivacySecret { + l.Warn("github-connector: secret privacy not allowed for nested teams, using closed", + zap.String("requested_privacy", privacy), + ) + } + newTeam.Privacy = github.Ptr(teamPrivacyClosed) + case privacy == teamPrivacySecret || privacy == teamPrivacyClosed: + // Non-nested teams can be "secret" or "closed" + newTeam.Privacy = github.Ptr(privacy) + default: + // Invalid privacy value for non-nested team + return nil, nil, fmt.Errorf("invalid privacy value: %q (must be \"secret\" or \"closed\")", privacy) + } + } else if isNestedTeam { + // Default for nested teams is "closed" + newTeam.Privacy = github.Ptr(teamPrivacyClosed) + } + // Note: Default for non-nested teams is "secret" (handled by GitHub API) + + if notificationsEnabled, ok := actions.GetBoolArg(args, "notifications_enabled"); ok { + if notificationsEnabled { + newTeam.NotificationSetting = github.Ptr("notifications_enabled") + } else { + newTeam.NotificationSetting = github.Ptr("notifications_disabled") + } + } + + if maintainerIDs, ok := actions.GetResourceIdListArg(args, "maintainers"); ok && len(maintainerIDs) > 0 { + var maintainerLogins []string + for _, rid := range maintainerIDs { + userID, err := strconv.ParseInt(rid.Resource, 10, 64) + if err != nil { + return nil, nil, fmt.Errorf("invalid maintainer user ID %s: %w", rid.Resource, err) + } + user, resp, err := o.client.Users.GetByID(ctx, userID) + if err != nil { + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to get user %d", userID)) + } + maintainerLogins = append(maintainerLogins, user.GetLogin()) + } + newTeam.Maintainers = maintainerLogins + } + + if repoIDs, ok := actions.GetResourceIdListArg(args, "repo_names"); ok && len(repoIDs) > 0 { + var repoFullNames []string + for _, rid := range repoIDs { + repoID, err := strconv.ParseInt(rid.Resource, 10, 64) + if err != nil { + return nil, nil, fmt.Errorf("invalid repository ID %s: %w", rid.Resource, err) + } + repo, resp, err := o.client.Repositories.GetByID(ctx, repoID) + if err != nil { + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to get repository %d", repoID)) + } + repoFullNames = append(repoFullNames, repo.GetFullName()) + } + newTeam.RepoNames = repoFullNames + } + + // Create the team via GitHub API + createdTeam, resp, err := o.client.Teams.CreateTeam(ctx, orgName, newTeam) + if err != nil { + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to create team %s in org %s", name, orgName)) + } + + // Extract rate limit data for annotations + var annos annotations.Annotations + if rateLimitData, err := extractRateLimitData(resp); err == nil { + annos.WithRateLimiting(rateLimitData) + } + + l.Info("github-connector: team created successfully via action", + zap.String("team_name", createdTeam.GetName()), + zap.Int64("team_id", createdTeam.GetID()), + zap.String("team_slug", createdTeam.GetSlug()), + ) + + // Create the resource representation of the newly created team + teamRes, err := teamResource(createdTeam, parentResourceID) + if err != nil { + return nil, annos, fmt.Errorf("failed to create resource representation: %w", err) + } + + // Generate entitlements for the newly created team (reuse existing method) + entitlements, _, _, err := o.Entitlements(ctx, teamRes, nil) + if err != nil { + return nil, annos, fmt.Errorf("failed to generate entitlements: %w", err) + } + + // Fetch grants for the newly created team by reusing the existing Grants method + var grants []*v2.Grant + pageToken := "" + for { + pToken := &pagination.Token{Token: pageToken} + pageGrants, nextToken, _, err := o.Grants(ctx, teamRes, pToken) + if err != nil { + l.Warn("github-connector: failed to fetch grants for team", zap.Error(err)) + break + } + grants = append(grants, pageGrants...) + if nextToken == "" { + break + } + pageToken = nextToken + } + + // Build return values using SDK helpers + resourceRv, err := actions.NewResourceReturnField("resource", teamRes) + if err != nil { + return nil, annos, err + } + + entitlementsRv, err := actions.NewEntitlementListReturnField("entitlements", entitlements) + if err != nil { + return nil, annos, err + } + + grantsRv, err := actions.NewGrantListReturnField("grants", grants) + if err != nil { + return nil, annos, err + } + + return actions.NewReturnValues(true, resourceRv, entitlementsRv, grantsRv), annos, nil +} + func teamBuilder(client *github.Client, orgCache *orgNameCache) *teamResourceType { return &teamResourceType{ resourceType: resourceTypeTeam, diff --git a/pkg/connector/team_test.go b/pkg/connector/team_test.go index 5700f14c..bcab741d 100644 --- a/pkg/connector/team_test.go +++ b/pkg/connector/team_test.go @@ -10,6 +10,7 @@ import ( resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" "github.com/conductorone/baton-github/test" "github.com/conductorone/baton-github/test/mocks" @@ -58,3 +59,213 @@ func TestTeam(t *testing.T) { require.Empty(t, revokeAnnotations) }) } + +func TestTeamActions(t *testing.T) { + ctx := context.Background() + + t.Run("should create a basic team with name, description, notifications, and privacy", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + githubOrganization, _, _, _, _, _ := mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := teamBuilder(githubClient, cache) + + // Create args for the action + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-team-basic", + "description": "A test team for unit testing", + "privacy": "secret", + "notifications_enabled": true, + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", // Matches the seeded org ID + }, + }) + require.NoError(t, err) + + result, annos, err := client.handleCreateTeamAction(ctx, args) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, annos) + + // Verify success field + successVal := result.Fields["success"] + require.NotNil(t, successVal) + require.True(t, successVal.GetBoolValue()) + + // Verify resource was returned + resourceVal := result.Fields["resource"] + require.NotNil(t, resourceVal) + + _ = githubOrganization // Used in seed + }) + + t.Run("should create a team with multiple maintainers", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, existingUser, _, _ := mgh.Seed() + + // Add a second user + secondUserID := int64(100) + secondUserLogin := "100" + secondUserEmail := "seconduser@example.com" + mgh.AddUser(github.User{ + ID: github.Ptr(secondUserID), + Login: github.Ptr(secondUserLogin), + Email: github.Ptr(secondUserEmail), + }) + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := teamBuilder(githubClient, cache) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-team-maintainers", + "description": "Team with multiple maintainers", + "privacy": "secret", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + "maintainers": []interface{}{ + map[string]interface{}{ + "resource_type": "user", + "resource": "56", // existingUser.ID + }, + map[string]interface{}{ + "resource_type": "user", + "resource": "100", // secondUserID + }, + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateTeamAction(ctx, args) + require.NoError(t, err) + require.NotNil(t, result) + require.True(t, result.Fields["success"].GetBoolValue()) + + _ = existingUser // Used in seed + }) + + t.Run("should fail to create nested team when parent team is secret", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + githubOrganization, _, _, _, _, _ := mgh.Seed() + + // Add a secret parent team + secretParentTeamID := int64(200) + mgh.AddTeam(github.Team{ + ID: github.Ptr(secretParentTeamID), + Name: github.Ptr("secret-parent-team"), + Slug: github.Ptr("secret-parent-team"), + Organization: githubOrganization, + Privacy: github.Ptr("secret"), + }) + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := teamBuilder(githubClient, cache) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "nested-team-under-secret", + "description": "This should fail because parent is secret", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + "parent": map[string]interface{}{ + "resource_type": "team", + "resource": "200", // Secret parent team ID + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateTeamAction(ctx, args) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "cannot create child team") + require.Contains(t, err.Error(), "secret") + }) + + t.Run("should successfully create nested team when parent team is closed", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + githubOrganization, _, _, _, _, _ := mgh.Seed() + + // Add a closed parent team + closedParentTeamID := int64(201) + mgh.AddTeam(github.Team{ + ID: github.Ptr(closedParentTeamID), + Name: github.Ptr("closed-parent-team"), + Slug: github.Ptr("closed-parent-team"), + Organization: githubOrganization, + Privacy: github.Ptr("closed"), + }) + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := teamBuilder(githubClient, cache) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "nested-team-under-closed", + "description": "Nested team under closed parent", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + "parent": map[string]interface{}{ + "resource_type": "team", + "resource": "201", // Closed parent team ID + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateTeamAction(ctx, args) + require.NoError(t, err) + require.NotNil(t, result) + require.True(t, result.Fields["success"].GetBoolValue()) + }) + + t.Run("should fail with missing required org field", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := teamBuilder(githubClient, cache) + + // Missing org field + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-team", + "description": "A team without org", + }) + require.NoError(t, err) + + result, _, err := client.handleCreateTeamAction(ctx, args) + require.Error(t, err) + require.Nil(t, result) + }) + + t.Run("should fail with invalid privacy value", func(t *testing.T) { + mgh := mocks.NewMockGitHub() + _, _, _, _, _, _ = mgh.Seed() + + githubClient := github.NewClient(mgh.Server()) + cache := newOrgNameCache(githubClient) + client := teamBuilder(githubClient, cache) + + args, err := structpb.NewStruct(map[string]interface{}{ + "name": "test-team-invalid-privacy", + "privacy": "invalid_privacy_value", + "org": map[string]interface{}{ + "resource_type": "org", + "resource": "12", + }, + }) + require.NoError(t, err) + + result, _, err := client.handleCreateTeamAction(ctx, args) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "invalid privacy value") + }) +} diff --git a/test/mocks/endpointpattern.go b/test/mocks/endpointpattern.go index c57cefff..2ac131a1 100644 --- a/test/mocks/endpointpattern.go +++ b/test/mocks/endpointpattern.go @@ -67,3 +67,21 @@ var DeleteOrgsRolesUsersByOrgByRoleIdByUsername = mock.EndpointPattern{ Pattern: "/orgs/{org}/organization-roles/users/{username}/{role_id}", Method: "DELETE", } + +// Team creation endpoint. +var PostOrgsTeamsByOrg = mock.EndpointPattern{ + Pattern: "/orgs/{org}/teams", + Method: "POST", +} + +// Repository creation endpoint. +var PostOrgsReposByOrg = mock.EndpointPattern{ + Pattern: "/orgs/{org}/repos", + Method: "POST", +} + +// Get organization by slug/login (not numeric ID). +var GetOrgsByName = mock.EndpointPattern{ + Pattern: "/orgs/{org}", + Method: "GET", +} diff --git a/test/mocks/github.go b/test/mocks/github.go index 45f5445a..6b455e81 100644 --- a/test/mocks/github.go +++ b/test/mocks/github.go @@ -753,6 +753,10 @@ func (mgh MockGitHub) Server() *http.Client { }: mgh.getOrgRoleByID, PutOrgsRolesUsersByOrgByRoleIdByUsername: mgh.addOrgRoleUser, DeleteOrgsRolesUsersByOrgByRoleIdByUsername: mgh.removeOrgRoleUser, + // Team and repository creation + PostOrgsTeamsByOrg: mgh.createTeam, + PostOrgsReposByOrg: mgh.createRepository, + GetOrgsByName: mgh.getOrganizationBySlug, } options := make([]mock.MockBackendOption, 0) @@ -782,3 +786,177 @@ func (mgh *MockGitHub) AddMembership(teamID int64, userID int64) { } mgh.teamMemberships[teamID].Add(userID) } + +// AddUser adds a user to the mock server for testing purposes. +func (mgh *MockGitHub) AddUser(user github.User) { + mgh.users[*user.ID] = user +} + +// AddRepository adds a repository to the mock server for testing purposes. +func (mgh *MockGitHub) AddRepository(repo github.Repository) { + mgh.repositories[*repo.ID] = repo +} + +// AddOrganization adds an organization to the mock server for testing purposes. +func (mgh *MockGitHub) AddOrganization(org github.Organization) { + mgh.organizations[*org.ID] = org +} + +// nextTeamID tracks the next team ID to assign for created teams. +var nextTeamID int64 = 1000 + +// nextRepoID tracks the next repo ID to assign for created repos. +var nextRepoID int64 = 1000 + +// createTeam creates a new team in the mock server. +func (mgh *MockGitHub) createTeam( + w http.ResponseWriter, + variables map[string]string, +) { + orgSlug, ok := variables["org"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Find org by slug + var foundOrg *github.Organization + for _, org := range mgh.organizations { + if org.GetLogin() == orgSlug { + foundOrg = &org + break + } + } + if foundOrg == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + teamName := variables["name"] + if teamName == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Get privacy setting + privacy := variables["privacy"] + if privacy == "" { + privacy = "secret" // Default for non-nested teams + } + + // Check if parent team is specified + parentTeamIDStr := variables["parent_team_id"] + var parentTeam *github.Team + if parentTeamIDStr != "" { + parentTeamID, err := strconv.ParseInt(parentTeamIDStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + pt, ok := mgh.teams[parentTeamID] + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + parentTeam = &pt + // Nested teams must be closed + privacy = "closed" + } + + teamID := nextTeamID + nextTeamID++ + + newTeam := github.Team{ + ID: github.Ptr(teamID), + Name: github.Ptr(teamName), + Slug: github.Ptr(strings.ToLower(strings.ReplaceAll(teamName, " ", "-"))), + Organization: foundOrg, + Privacy: github.Ptr(privacy), + } + if parentTeam != nil { + newTeam.Parent = parentTeam + } + + mgh.teams[teamID] = newTeam + mgh.teamMemberships[teamID] = mapset.NewSet[int64]() + + _, _ = w.Write(mock.MustMarshal(newTeam)) +} + +// createRepository creates a new repository in the mock server. +func (mgh *MockGitHub) createRepository( + w http.ResponseWriter, + variables map[string]string, +) { + orgSlug, ok := variables["org"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Find org by slug + var foundOrg *github.Organization + for _, org := range mgh.organizations { + if org.GetLogin() == orgSlug { + foundOrg = &org + break + } + } + if foundOrg == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + repoName := variables["name"] + if repoName == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + visibility := variables["visibility"] + if visibility == "" { + visibility = "private" + } + + description := variables["description"] + + repoID := nextRepoID + nextRepoID++ + + fullName := fmt.Sprintf("%s/%s", foundOrg.GetLogin(), repoName) + newRepo := github.Repository{ + ID: github.Ptr(repoID), + Name: github.Ptr(repoName), + FullName: github.Ptr(fullName), + Description: github.Ptr(description), + Organization: foundOrg, + Visibility: github.Ptr(visibility), + Private: github.Ptr(visibility == "private"), + } + + mgh.repositories[repoID] = newRepo + mgh.repositoryMemberships[repoID] = mapset.NewSet[int64]() + + _, _ = w.Write(mock.MustMarshal(newRepo)) +} + +// getOrganizationBySlug gets an organization by its slug/login. +func (mgh *MockGitHub) getOrganizationBySlug( + w http.ResponseWriter, + variables map[string]string, +) { + orgSlug, ok := variables["org"] + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + // Find org by slug + for _, org := range mgh.organizations { + if org.GetLogin() == orgSlug { + _, _ = w.Write(mock.MustMarshal(org)) + return + } + } + w.WriteHeader(http.StatusNotFound) +} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config.pb.go index d563d18f..c54d93ac 100644 --- a/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config.pb.go +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config.pb.go @@ -31,6 +31,7 @@ const ( ConstraintKind_CONSTRAINT_KIND_AT_LEAST_ONE ConstraintKind = 2 ConstraintKind_CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE ConstraintKind = 3 ConstraintKind_CONSTRAINT_KIND_DEPENDENT_ON ConstraintKind = 4 + ConstraintKind_CONSTRAINT_KIND_ALLOWED_OPTIONS ConstraintKind = 5 ) // Enum value maps for ConstraintKind. @@ -41,6 +42,7 @@ var ( 2: "CONSTRAINT_KIND_AT_LEAST_ONE", 3: "CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE", 4: "CONSTRAINT_KIND_DEPENDENT_ON", + 5: "CONSTRAINT_KIND_ALLOWED_OPTIONS", } ConstraintKind_value = map[string]int32{ "CONSTRAINT_KIND_UNSPECIFIED": 0, @@ -48,6 +50,7 @@ var ( "CONSTRAINT_KIND_AT_LEAST_ONE": 2, "CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE": 3, "CONSTRAINT_KIND_DEPENDENT_ON": 4, + "CONSTRAINT_KIND_ALLOWED_OPTIONS": 5, } ) @@ -314,6 +317,7 @@ type Constraint struct { Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` // optional HelpText string `protobuf:"bytes,5,opt,name=help_text,json=helpText,proto3" json:"help_text,omitempty"` // optional IsFieldGroup bool `protobuf:"varint,6,opt,name=is_field_group,json=isFieldGroup,proto3" json:"is_field_group,omitempty"` + AllowedOptionValues []string `protobuf:"bytes,7,rep,name=allowed_option_values,json=allowedOptionValues,proto3" json:"allowed_option_values,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -385,6 +389,13 @@ func (x *Constraint) GetIsFieldGroup() bool { return false } +func (x *Constraint) GetAllowedOptionValues() []string { + if x != nil { + return x.AllowedOptionValues + } + return nil +} + func (x *Constraint) SetKind(v ConstraintKind) { x.Kind = v } @@ -409,6 +420,10 @@ func (x *Constraint) SetIsFieldGroup(v bool) { x.IsFieldGroup = v } +func (x *Constraint) SetAllowedOptionValues(v []string) { + x.AllowedOptionValues = v +} + type Constraint_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. @@ -418,6 +433,7 @@ type Constraint_builder struct { Name string HelpText string IsFieldGroup bool + AllowedOptionValues []string } func (b0 Constraint_builder) Build() *Constraint { @@ -430,6 +446,7 @@ func (b0 Constraint_builder) Build() *Constraint { x.Name = b.Name x.HelpText = b.HelpText x.IsFieldGroup = b.IsFieldGroup + x.AllowedOptionValues = b.AllowedOptionValues return m0 } @@ -2735,7 +2752,7 @@ const file_c1_config_v1_config_proto_rawDesc = "" + "\x1bsupports_external_resources\x18\t \x01(\bR\x19supportsExternalResources\x12>\n" + "\x1brequires_external_connector\x18\n" + " \x01(\bR\x19requiresExternalConnector\x12;\n" + - "\ffield_groups\x18\v \x03(\v2\x18.c1.config.v1.FieldGroupR\vfieldGroups\"\xea\x01\n" + + "\ffield_groups\x18\v \x03(\v2\x18.c1.config.v1.FieldGroupR\vfieldGroups\"\x9e\x02\n" + "\n" + "Constraint\x120\n" + "\x04kind\x18\x01 \x01(\x0e2\x1c.c1.config.v1.ConstraintKindR\x04kind\x12\x1f\n" + @@ -2744,7 +2761,8 @@ const file_c1_config_v1_config_proto_rawDesc = "" + "\x15secondary_field_names\x18\x03 \x03(\tR\x13secondaryFieldNames\x12\x12\n" + "\x04name\x18\x04 \x01(\tR\x04name\x12\x1b\n" + "\thelp_text\x18\x05 \x01(\tR\bhelpText\x12$\n" + - "\x0eis_field_group\x18\x06 \x01(\bR\fisFieldGroup\"\x92\x01\n" + + "\x0eis_field_group\x18\x06 \x01(\bR\fisFieldGroup\x122\n" + + "\x15allowed_option_values\x18\a \x03(\tR\x13allowedOptionValues\"\x92\x01\n" + "\n" + "FieldGroup\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12!\n" + @@ -2847,13 +2865,14 @@ const file_c1_config_v1_config_proto_rawDesc = "" + "\x04type\x18\x03 \x01(\x0e2\x1d.c1.config.v1.StringFieldTypeR\x04type\x12-\n" + "\x12allowed_extensions\x18\x04 \x03(\tR\x11allowedExtensions\x129\n" + "\aoptions\x18\x05 \x03(\v2\x1f.c1.config.v1.StringFieldOptionR\aoptionsB\b\n" + - "\x06_rules*\xc4\x01\n" + + "\x06_rules*\xe9\x01\n" + "\x0eConstraintKind\x12\x1f\n" + "\x1bCONSTRAINT_KIND_UNSPECIFIED\x10\x00\x12%\n" + "!CONSTRAINT_KIND_REQUIRED_TOGETHER\x10\x01\x12 \n" + "\x1cCONSTRAINT_KIND_AT_LEAST_ONE\x10\x02\x12&\n" + "\"CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE\x10\x03\x12 \n" + - "\x1cCONSTRAINT_KIND_DEPENDENT_ON\x10\x04*\xc9\x01\n" + + "\x1cCONSTRAINT_KIND_DEPENDENT_ON\x10\x04\x12#\n" + + "\x1fCONSTRAINT_KIND_ALLOWED_OPTIONS\x10\x05*\xc9\x01\n" + "\x0fStringFieldType\x12&\n" + "\"STRING_FIELD_TYPE_TEXT_UNSPECIFIED\x10\x00\x12\x1c\n" + "\x18STRING_FIELD_TYPE_RANDOM\x10\x01\x12\x1c\n" + diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config_protoopaque.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config_protoopaque.pb.go index a236f5f3..4ef41382 100644 --- a/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config_protoopaque.pb.go +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/config/v1/config_protoopaque.pb.go @@ -31,6 +31,7 @@ const ( ConstraintKind_CONSTRAINT_KIND_AT_LEAST_ONE ConstraintKind = 2 ConstraintKind_CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE ConstraintKind = 3 ConstraintKind_CONSTRAINT_KIND_DEPENDENT_ON ConstraintKind = 4 + ConstraintKind_CONSTRAINT_KIND_ALLOWED_OPTIONS ConstraintKind = 5 ) // Enum value maps for ConstraintKind. @@ -41,6 +42,7 @@ var ( 2: "CONSTRAINT_KIND_AT_LEAST_ONE", 3: "CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE", 4: "CONSTRAINT_KIND_DEPENDENT_ON", + 5: "CONSTRAINT_KIND_ALLOWED_OPTIONS", } ConstraintKind_value = map[string]int32{ "CONSTRAINT_KIND_UNSPECIFIED": 0, @@ -48,6 +50,7 @@ var ( "CONSTRAINT_KIND_AT_LEAST_ONE": 2, "CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE": 3, "CONSTRAINT_KIND_DEPENDENT_ON": 4, + "CONSTRAINT_KIND_ALLOWED_OPTIONS": 5, } ) @@ -320,6 +323,7 @@ type Constraint struct { xxx_hidden_Name string `protobuf:"bytes,4,opt,name=name,proto3"` xxx_hidden_HelpText string `protobuf:"bytes,5,opt,name=help_text,json=helpText,proto3"` xxx_hidden_IsFieldGroup bool `protobuf:"varint,6,opt,name=is_field_group,json=isFieldGroup,proto3"` + xxx_hidden_AllowedOptionValues []string `protobuf:"bytes,7,rep,name=allowed_option_values,json=allowedOptionValues,proto3"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -391,6 +395,13 @@ func (x *Constraint) GetIsFieldGroup() bool { return false } +func (x *Constraint) GetAllowedOptionValues() []string { + if x != nil { + return x.xxx_hidden_AllowedOptionValues + } + return nil +} + func (x *Constraint) SetKind(v ConstraintKind) { x.xxx_hidden_Kind = v } @@ -415,6 +426,10 @@ func (x *Constraint) SetIsFieldGroup(v bool) { x.xxx_hidden_IsFieldGroup = v } +func (x *Constraint) SetAllowedOptionValues(v []string) { + x.xxx_hidden_AllowedOptionValues = v +} + type Constraint_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. @@ -424,6 +439,7 @@ type Constraint_builder struct { Name string HelpText string IsFieldGroup bool + AllowedOptionValues []string } func (b0 Constraint_builder) Build() *Constraint { @@ -436,6 +452,7 @@ func (b0 Constraint_builder) Build() *Constraint { x.xxx_hidden_Name = b.Name x.xxx_hidden_HelpText = b.HelpText x.xxx_hidden_IsFieldGroup = b.IsFieldGroup + x.xxx_hidden_AllowedOptionValues = b.AllowedOptionValues return m0 } @@ -2731,7 +2748,7 @@ const file_c1_config_v1_config_proto_rawDesc = "" + "\x1bsupports_external_resources\x18\t \x01(\bR\x19supportsExternalResources\x12>\n" + "\x1brequires_external_connector\x18\n" + " \x01(\bR\x19requiresExternalConnector\x12;\n" + - "\ffield_groups\x18\v \x03(\v2\x18.c1.config.v1.FieldGroupR\vfieldGroups\"\xea\x01\n" + + "\ffield_groups\x18\v \x03(\v2\x18.c1.config.v1.FieldGroupR\vfieldGroups\"\x9e\x02\n" + "\n" + "Constraint\x120\n" + "\x04kind\x18\x01 \x01(\x0e2\x1c.c1.config.v1.ConstraintKindR\x04kind\x12\x1f\n" + @@ -2740,7 +2757,8 @@ const file_c1_config_v1_config_proto_rawDesc = "" + "\x15secondary_field_names\x18\x03 \x03(\tR\x13secondaryFieldNames\x12\x12\n" + "\x04name\x18\x04 \x01(\tR\x04name\x12\x1b\n" + "\thelp_text\x18\x05 \x01(\tR\bhelpText\x12$\n" + - "\x0eis_field_group\x18\x06 \x01(\bR\fisFieldGroup\"\x92\x01\n" + + "\x0eis_field_group\x18\x06 \x01(\bR\fisFieldGroup\x122\n" + + "\x15allowed_option_values\x18\a \x03(\tR\x13allowedOptionValues\"\x92\x01\n" + "\n" + "FieldGroup\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12!\n" + @@ -2843,13 +2861,14 @@ const file_c1_config_v1_config_proto_rawDesc = "" + "\x04type\x18\x03 \x01(\x0e2\x1d.c1.config.v1.StringFieldTypeR\x04type\x12-\n" + "\x12allowed_extensions\x18\x04 \x03(\tR\x11allowedExtensions\x129\n" + "\aoptions\x18\x05 \x03(\v2\x1f.c1.config.v1.StringFieldOptionR\aoptionsB\b\n" + - "\x06_rules*\xc4\x01\n" + + "\x06_rules*\xe9\x01\n" + "\x0eConstraintKind\x12\x1f\n" + "\x1bCONSTRAINT_KIND_UNSPECIFIED\x10\x00\x12%\n" + "!CONSTRAINT_KIND_REQUIRED_TOGETHER\x10\x01\x12 \n" + "\x1cCONSTRAINT_KIND_AT_LEAST_ONE\x10\x02\x12&\n" + "\"CONSTRAINT_KIND_MUTUALLY_EXCLUSIVE\x10\x03\x12 \n" + - "\x1cCONSTRAINT_KIND_DEPENDENT_ON\x10\x04*\xc9\x01\n" + + "\x1cCONSTRAINT_KIND_DEPENDENT_ON\x10\x04\x12#\n" + + "\x1fCONSTRAINT_KIND_ALLOWED_OPTIONS\x10\x05*\xc9\x01\n" + "\x0fStringFieldType\x12&\n" + "\"STRING_FIELD_TYPE_TEXT_UNSPECIFIED\x10\x00\x12\x1c\n" + "\x18STRING_FIELD_TYPE_RANDOM\x10\x01\x12\x1c\n" + diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias.pb.go new file mode 100644 index 00000000..0c43fabb --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: c1/connector/v2/annotation_id_alias.proto + +//go:build !protoopaque + +package v2 + +import ( + _ "github.com/envoyproxy/protoc-gen-validate/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + unsafe "unsafe" +) + +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 Aliases struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Aliases) Reset() { + *x = Aliases{} + mi := &file_c1_connector_v2_annotation_id_alias_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Aliases) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Aliases) ProtoMessage() {} + +func (x *Aliases) ProtoReflect() protoreflect.Message { + mi := &file_c1_connector_v2_annotation_id_alias_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *Aliases) GetIds() []string { + if x != nil { + return x.Ids + } + return nil +} + +func (x *Aliases) SetIds(v []string) { + x.Ids = v +} + +type Aliases_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Ids []string +} + +func (b0 Aliases_builder) Build() *Aliases { + m0 := &Aliases{} + b, x := &b0, m0 + _, _ = b, x + x.Ids = b.Ids + return m0 +} + +var File_c1_connector_v2_annotation_id_alias_proto protoreflect.FileDescriptor + +const file_c1_connector_v2_annotation_id_alias_proto_rawDesc = "" + + "\n" + + ")c1/connector/v2/annotation_id_alias.proto\x12\x0fc1.connector.v2\x1a\x17validate/validate.proto\"2\n" + + "\aAliases\x12'\n" + + "\x03ids\x18\x01 \x03(\tB\x15\xfaB\x12\x92\x01\x0f\b\x01\x10d\x18\x01\"\ar\x05\x10\x01\x18\x80\x02R\x03idsB6Z4github.com/conductorone/baton-sdk/pb/c1/connector/v2b\x06proto3" + +var file_c1_connector_v2_annotation_id_alias_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_c1_connector_v2_annotation_id_alias_proto_goTypes = []any{ + (*Aliases)(nil), // 0: c1.connector.v2.Aliases +} +var file_c1_connector_v2_annotation_id_alias_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_id_alias_proto_init() } +func file_c1_connector_v2_annotation_id_alias_proto_init() { + if File_c1_connector_v2_annotation_id_alias_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_c1_connector_v2_annotation_id_alias_proto_rawDesc), len(file_c1_connector_v2_annotation_id_alias_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_c1_connector_v2_annotation_id_alias_proto_goTypes, + DependencyIndexes: file_c1_connector_v2_annotation_id_alias_proto_depIdxs, + MessageInfos: file_c1_connector_v2_annotation_id_alias_proto_msgTypes, + }.Build() + File_c1_connector_v2_annotation_id_alias_proto = out.File + file_c1_connector_v2_annotation_id_alias_proto_goTypes = nil + file_c1_connector_v2_annotation_id_alias_proto_depIdxs = nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias.pb.validate.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias.pb.validate.go new file mode 100644 index 00000000..03d13060 --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias.pb.validate.go @@ -0,0 +1,176 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: c1/connector/v2/annotation_id_alias.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 Aliases 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 *Aliases) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Aliases 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 AliasesMultiError, or nil if none found. +func (m *Aliases) ValidateAll() error { + return m.validate(true) +} + +func (m *Aliases) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if l := len(m.GetIds()); l < 1 || l > 100 { + err := AliasesValidationError{ + field: "Ids", + reason: "value must contain between 1 and 100 items, inclusive", + } + if !all { + return err + } + errors = append(errors, err) + } + + _Aliases_Ids_Unique := make(map[string]struct{}, len(m.GetIds())) + + for idx, item := range m.GetIds() { + _, _ = idx, item + + if _, exists := _Aliases_Ids_Unique[item]; exists { + err := AliasesValidationError{ + field: fmt.Sprintf("Ids[%v]", idx), + reason: "repeated value must contain unique items", + } + if !all { + return err + } + errors = append(errors, err) + } else { + _Aliases_Ids_Unique[item] = struct{}{} + } + + if l := utf8.RuneCountInString(item); l < 1 || l > 256 { + err := AliasesValidationError{ + field: fmt.Sprintf("Ids[%v]", idx), + reason: "value length must be between 1 and 256 runes, inclusive", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + + if len(errors) > 0 { + return AliasesMultiError(errors) + } + + return nil +} + +// AliasesMultiError is an error wrapping multiple validation errors returned +// by Aliases.ValidateAll() if the designated constraints aren't met. +type AliasesMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m AliasesMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m AliasesMultiError) AllErrors() []error { return m } + +// AliasesValidationError is the validation error returned by Aliases.Validate +// if the designated constraints aren't met. +type AliasesValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e AliasesValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e AliasesValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e AliasesValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e AliasesValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e AliasesValidationError) ErrorName() string { return "AliasesValidationError" } + +// Error satisfies the builtin error interface +func (e AliasesValidationError) 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 %sAliases.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = AliasesValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = AliasesValidationError{} diff --git a/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias_protoopaque.pb.go b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias_protoopaque.pb.go new file mode 100644 index 00000000..6cfa13dc --- /dev/null +++ b/vendor/github.com/conductorone/baton-sdk/pb/c1/connector/v2/annotation_id_alias_protoopaque.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: c1/connector/v2/annotation_id_alias.proto + +//go:build protoopaque + +package v2 + +import ( + _ "github.com/envoyproxy/protoc-gen-validate/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + unsafe "unsafe" +) + +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 Aliases struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Ids []string `protobuf:"bytes,1,rep,name=ids,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Aliases) Reset() { + *x = Aliases{} + mi := &file_c1_connector_v2_annotation_id_alias_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Aliases) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Aliases) ProtoMessage() {} + +func (x *Aliases) ProtoReflect() protoreflect.Message { + mi := &file_c1_connector_v2_annotation_id_alias_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *Aliases) GetIds() []string { + if x != nil { + return x.xxx_hidden_Ids + } + return nil +} + +func (x *Aliases) SetIds(v []string) { + x.xxx_hidden_Ids = v +} + +type Aliases_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Ids []string +} + +func (b0 Aliases_builder) Build() *Aliases { + m0 := &Aliases{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Ids = b.Ids + return m0 +} + +var File_c1_connector_v2_annotation_id_alias_proto protoreflect.FileDescriptor + +const file_c1_connector_v2_annotation_id_alias_proto_rawDesc = "" + + "\n" + + ")c1/connector/v2/annotation_id_alias.proto\x12\x0fc1.connector.v2\x1a\x17validate/validate.proto\"2\n" + + "\aAliases\x12'\n" + + "\x03ids\x18\x01 \x03(\tB\x15\xfaB\x12\x92\x01\x0f\b\x01\x10d\x18\x01\"\ar\x05\x10\x01\x18\x80\x02R\x03idsB6Z4github.com/conductorone/baton-sdk/pb/c1/connector/v2b\x06proto3" + +var file_c1_connector_v2_annotation_id_alias_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_c1_connector_v2_annotation_id_alias_proto_goTypes = []any{ + (*Aliases)(nil), // 0: c1.connector.v2.Aliases +} +var file_c1_connector_v2_annotation_id_alias_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_id_alias_proto_init() } +func file_c1_connector_v2_annotation_id_alias_proto_init() { + if File_c1_connector_v2_annotation_id_alias_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_c1_connector_v2_annotation_id_alias_proto_rawDesc), len(file_c1_connector_v2_annotation_id_alias_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_c1_connector_v2_annotation_id_alias_proto_goTypes, + DependencyIndexes: file_c1_connector_v2_annotation_id_alias_proto_depIdxs, + MessageInfos: file_c1_connector_v2_annotation_id_alias_proto_msgTypes, + }.Build() + File_c1_connector_v2_annotation_id_alias_proto = out.File + file_c1_connector_v2_annotation_id_alias_proto_goTypes = nil + file_c1_connector_v2_annotation_id_alias_proto_depIdxs = nil +} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/actions/actions.go b/vendor/github.com/conductorone/baton-sdk/pkg/actions/actions.go index 5d997718..2621e404 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/actions/actions.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/actions/actions.go @@ -563,14 +563,14 @@ func validateActionConstraints(constraints []*config.Constraint, args *structpb. // Validate each constraint for _, constraint := range constraints { - if err := validateConstraint(constraint, present); err != nil { + if err := validateConstraint(constraint, present, args); err != nil { return err } } return nil } -func validateConstraint(c *config.Constraint, present map[string]bool) error { +func validateConstraint(c *config.Constraint, present map[string]bool, args *structpb.Struct) error { // Deduplicate field names to handle cases where the same field is listed multiple times uniqueFieldNames := deduplicateStrings(c.GetFieldNames()) @@ -605,6 +605,36 @@ func validateConstraint(c *config.Constraint, present map[string]bool) error { } } } + case config.ConstraintKind_CONSTRAINT_KIND_ALLOWED_OPTIONS: + if primaryPresent > 0 { + // When secondary fields are set, only allowed option values are permitted + uniqueSecondaryFieldNames := deduplicateStrings(c.GetSecondaryFieldNames()) + anySecondaryPresent := false + for _, name := range uniqueSecondaryFieldNames { + if present[name] { + anySecondaryPresent = true + break + } + } + if anySecondaryPresent { + allowedValues := make(map[string]bool) + for _, v := range c.GetAllowedOptionValues() { + allowedValues[v] = true + } + if args != nil { + for _, fieldName := range uniqueFieldNames { + if val, ok := args.GetFields()[fieldName]; ok { + if strVal, ok := val.GetKind().(*structpb.Value_StringValue); ok { + if !allowedValues[strVal.StringValue] { + return fmt.Errorf("option %q for field %q is not allowed when %v is set; allowed values: %v", + strVal.StringValue, fieldName, uniqueSecondaryFieldNames, c.GetAllowedOptionValues()) + } + } + } + } + } + } + } case config.ConstraintKind_CONSTRAINT_KIND_UNSPECIFIED: return nil default: 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 1b7aadcd..51836a2a 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/cli/commands.go @@ -33,6 +33,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/logging" "github.com/conductorone/baton-sdk/pkg/session" "github.com/conductorone/baton-sdk/pkg/types/sessions" + "github.com/conductorone/baton-sdk/pkg/uhttp" "github.com/conductorone/baton-sdk/pkg/uotel" utls2 "github.com/conductorone/baton-sdk/pkg/utls" ) @@ -380,6 +381,16 @@ func MakeMainCommand[T field.Configurable]( opts = append(opts, connectorrunner.WithSkipGrants(v.GetBool("skip-grants"))) } + httpTimeout := v.GetInt(field.HttpTimeoutField.GetName()) + if httpTimeout <= 0 { + return fmt.Errorf("field %s: value must be greater than or equal to 1 but got %d", field.HttpTimeoutField.GetName(), httpTimeout) + } + httpTimeoutField := field.HttpTimeoutField + if _, err := field.ValidateField(&httpTimeoutField, httpTimeout); err != nil { + return err + } + runCtx = context.WithValue(runCtx, uhttp.ContextHTTPTimeoutKey, time.Duration(httpTimeout)*time.Second) + // Save the selected authentication method and get the connector. c, err := getconnector(runCtx, t, RunTimeOpts{SelectedAuthMethod: v.GetString("auth-method")}) if err != nil { @@ -540,6 +551,16 @@ func MakeGRPCServerCommand[T field.Configurable]( runCtx = context.WithValue(runCtx, crypto.ContextClientSecretKey, secretJwk) } + httpTimeout := v.GetInt(field.HttpTimeoutField.GetName()) + if httpTimeout <= 0 { + return fmt.Errorf("field %s: value must be greater than or equal to 1 but got %d", field.HttpTimeoutField.GetName(), httpTimeout) + } + httpTimeoutField := field.HttpTimeoutField + if _, err := field.ValidateField(&httpTimeoutField, httpTimeout); err != nil { + return err + } + runCtx = context.WithValue(runCtx, uhttp.ContextHTTPTimeoutKey, time.Duration(httpTimeout)*time.Second) + sessionStoreMaximumSize := v.GetInt(field.ServerSessionStoreMaximumSizeField.GetName()) sessionConstructor := getGRPCSessionStoreClient(runCtx, serverCfg) c, err := getconnector(runCtx, t, RunTimeOpts{ diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go index 19876916..efd217e5 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/dotc1z/c1file.go @@ -253,7 +253,8 @@ func cleanupDbDir(dbFilePath string, err error) error { var ErrReadOnly = errors.New("c1z: read only mode") // Close ensures that the sqlite database is flushed to disk, and if any changes were made we update the original database -// with our changes. The provided context is used for the WAL checkpoint operation. +// with our changes. The provided context is used for the WAL checkpoint operation. If the context is already expired, +// a fresh context with a 30-second timeout is used to ensure the checkpoint completes. func (c *C1File) Close(ctx context.Context) error { var err error @@ -277,7 +278,17 @@ func (c *C1File) Close(ctx context.Context) error { // the WAL file to zero bytes. This guarantees all data is in the main // database file before we read it for compression. if c.dbUpdated && !c.readOnly { - _, err = c.rawDb.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)") + // Use a dedicated context for the checkpoint. The caller's context + // may already be expired (e.g. Temporal activity deadline), but the + // checkpoint is a local SQLite operation that must complete to avoid + // saving a stale c1z. + checkpointCtx := ctx + if ctx.Err() != nil { + var checkpointCancel context.CancelFunc + checkpointCtx, checkpointCancel = context.WithTimeout(context.Background(), 30*time.Second) + defer checkpointCancel() + } + _, err = c.rawDb.ExecContext(checkpointCtx, "PRAGMA wal_checkpoint(TRUNCATE)") if err != nil { l := ctxzap.Extract(ctx) // Checkpoint failed - log and continue. The subsequent Close() diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go b/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go index d08a0c8b..f5680ce4 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/defaults.go @@ -309,6 +309,15 @@ var ( WithPersistent(true), WithHidden(true), WithExportTarget(ExportTargetOps)) + + HttpTimeoutField = IntField("http-timeout-seconds", + WithDescription("HTTP client timeout in seconds (max 1800)"), + WithDefaultValue(300), + WithPersistent(true), + WithExportTarget(ExportTargetOps), + WithInt(func(r *IntRuler) { + r.Gte(1).Lte(1800) + })) ) func LambdaServerFields() []SchemaField { @@ -396,6 +405,8 @@ var DefaultFields = []SchemaField{ healthCheckField, healthCheckPortField, healthCheckBindAddressField, + + HttpTimeoutField, } func IsFieldAmongDefaultList(f SchemaField) bool { 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 a43363e2..60367290 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/field/fields.go @@ -10,6 +10,11 @@ import ( var ErrWrongValueType = errors.New("unable to cast any to concrete type") +const ( + Oauth2ClientIDFieldName = "oauth2_client_cred_grant_client_id" + Oauth2ClientSecretFieldName = "oauth2_client_cred_grant_client_secret" //nolint:gosec // this is not a credential +) + type Variant string const ( diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go b/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go index f07d8e61..97e19532 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/sdk/version.go @@ -1,3 +1,3 @@ package sdk -const Version = "v0.7.15" +const Version = "v0.7.18" diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/resource.go b/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/resource.go index b7dae67e..d314a3dc 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/resource.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/types/resource/resource.go @@ -14,6 +14,9 @@ import ( "google.golang.org/protobuf/proto" ) +var ErrNoAlias = fmt.Errorf("no aliases found for resource") +var ErrEmptyAlias = fmt.Errorf("alias cannot be empty") + type ResourceOption func(*v2.Resource) error func WithAnnotation(msgs ...proto.Message) ResourceOption { @@ -216,6 +219,53 @@ func WithSecretTrait(opts ...SecretTraitOption) ResourceOption { } } +// WithAliases sets the aliases id for a resource. +func WithAliases(aliases ...string) ResourceOption { + return func(r *v2.Resource) error { + if len(aliases) == 0 { + return ErrNoAlias + } + + aliasV := &v2.Aliases{} + + annos := annotations.Annotations(r.GetAnnotations()) + _, err := annos.Pick(aliasV) + if err != nil { + return err + } + + uniqueAlias := make(map[string]struct{}, len(aliasV.GetIds())+len(aliases)) + for _, alias := range aliasV.GetIds() { + uniqueAlias[alias] = struct{}{} + } + + for _, alias := range aliases { + if alias == "" { + return ErrEmptyAlias + } + + uniqueAlias[alias] = struct{}{} + } + + ids := make([]string, 0, len(uniqueAlias)) + for alias := range uniqueAlias { + ids = append(ids, alias) + } + + aliasV.Ids = ids + + err = aliasV.Validate() + if err != nil { + return err + } + + annos.Update(aliasV) + r.SetAnnotations(annos) + + return nil + } +} + func convertIDToString(id interface{}) (string, error) { var resourceID string switch objID := id.(type) { diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go index 68605695..df9474ef 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/client.go @@ -13,6 +13,12 @@ import ( "go.uber.org/zap" ) +type contextKeyType struct{} + +// ContextHTTPTimeoutKey is the context key used to pass the HTTP timeout duration +// from the CLI configuration to uhttp.NewClient. +var ContextHTTPTimeoutKey = contextKeyType{} + type tlsClientConfigOption struct { config *tls.Config } @@ -60,21 +66,41 @@ func WithUserAgent(userAgent string) Option { } } +type timeoutOption struct { + timeout time.Duration +} + +func (o timeoutOption) Apply(c *Transport) { + c.timeout = o.timeout +} + +// WithTimeout sets the HTTP client timeout. Defaults to 300s (5 minutes) if not specified. +func WithTimeout(timeout time.Duration) Option { + return timeoutOption{timeout: timeout} +} + type Option interface { Apply(*Transport) } // NewClient creates a new HTTP client that uses the given context and options to create a new transport layer. func NewClient(ctx context.Context, options ...Option) (*http.Client, error) { - httpClient := &http.Client{ - Timeout: 300 * time.Second, // 5 minutes - } t, err := NewTransport(ctx, options...) if err != nil { return nil, err } - httpClient.Transport = t - return httpClient, nil + + timeout := 300 * time.Second // 5 minutes default + if t.timeout > 0 { + timeout = t.timeout + } else if ctxTimeout, ok := ctx.Value(ContextHTTPTimeoutKey).(time.Duration); ok && ctxTimeout > 0 { + timeout = ctxTimeout + } + + return &http.Client{ + Timeout: timeout, + Transport: t, + }, nil } type icache interface { diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go index 7066921b..ad20a281 100644 --- a/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go +++ b/vendor/github.com/conductorone/baton-sdk/pkg/uhttp/transport.go @@ -57,6 +57,7 @@ type Transport struct { roundTripper http.RoundTripper logger *zap.Logger log bool + timeout time.Duration nextCycle time.Time mtx sync.RWMutex } diff --git a/vendor/modules.txt b/vendor/modules.txt index 744eee01..03052f53 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -162,7 +162,7 @@ github.com/cenkalti/backoff/v5 # github.com/cespare/xxhash/v2 v2.3.0 ## explicit; go 1.11 github.com/cespare/xxhash/v2 -# github.com/conductorone/baton-sdk v0.7.16 +# github.com/conductorone/baton-sdk v0.7.19-0.20260209222658-3300146ac692 ## explicit; go 1.25.2 github.com/conductorone/baton-sdk/internal/connector github.com/conductorone/baton-sdk/pb/c1/c1z/v1