diff --git a/cmd/baton-github/main.go b/cmd/baton-github/main.go index 5d853c10..56742ec8 100644 --- a/cmd/baton-github/main.go +++ b/cmd/baton-github/main.go @@ -2,16 +2,10 @@ package main import ( "context" - "fmt" - "os" cfg "github.com/conductorone/baton-github/pkg/config" "github.com/conductorone/baton-sdk/pkg/config" - "github.com/conductorone/baton-sdk/pkg/connectorbuilder" - "github.com/conductorone/baton-sdk/pkg/field" - "github.com/conductorone/baton-sdk/pkg/types" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" + "github.com/conductorone/baton-sdk/pkg/connectorrunner" "github.com/conductorone/baton-github/pkg/connector" ) @@ -20,55 +14,5 @@ var version = "dev" func main() { ctx := context.Background() - - _, cmd, err := config.DefineConfiguration( - ctx, - "baton-github", - getConnector, - cfg.Config, - ) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) - } - cmd.Version = version - - err = cmd.Execute() - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) - } -} - -func getConnector(ctx context.Context, ghc *cfg.Github) (types.ConnectorServer, error) { - l := ctxzap.Extract(ctx) - - err := field.Validate(cfg.Config, ghc) - if err != nil { - return nil, err - } - - privateKey := "" - if ghc.AppPrivatekeyPath != "" { - keyBytes, err := os.ReadFile(ghc.AppPrivatekeyPath) - if err != nil { - l.Error("error reading app private key file", zap.Error(err), zap.String("appPrivateKeyPath", ghc.AppPrivatekeyPath)) - return nil, fmt.Errorf("failed to read app private key file: %w", err) - } - privateKey = string(keyBytes) - } - - cb, err := connector.New(ctx, ghc, privateKey) - if err != nil { - l.Error("error creating connector", zap.Error(err)) - return nil, err - } - - c, err := connectorbuilder.NewConnector(ctx, cb) - if err != nil { - l.Error("error creating connector", zap.Error(err)) - return nil, err - } - - return c, nil + config.RunConnector(ctx, "baton-github", version, cfg.Config, connector.NewLambdaConnector, connectorrunner.WithSessionStoreEnabled()) } diff --git a/pkg/config/conf.gen.go b/pkg/config/conf.gen.go index d6d0b78c..4a963e2f 100644 --- a/pkg/config/conf.gen.go +++ b/pkg/config/conf.gen.go @@ -1,17 +1,18 @@ // 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 string `mapstructure:"app-privatekey-path"` + 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"` } func (c *Github) findFieldByTag(tagValue string) (any, bool) { @@ -46,11 +47,13 @@ func (c *Github) GetString(fieldName string) string { if !ok { return "" } - t, ok := v.(string) - if !ok { - panic("wrong type") + if t, ok := v.(string); ok { + return t } - 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/config/config.go b/pkg/config/config.go index 2e4dd814..0093c467 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,11 @@ import ( "github.com/conductorone/baton-sdk/pkg/field" ) +const ( + GithubAppGroup = "github-app-group" + GithubPersonalAccessTokenGroup = "personal-access-token-group" +) + // TODO (mb): Make sure we don't need field.WithRequired(true) for required fields. var ( accessTokenField = field.StringField( @@ -11,6 +16,7 @@ var ( field.WithDisplayName("Personal access token"), field.WithDescription("The GitHub access token used to connect to the GitHub API."), field.WithIsSecret(true), + field.WithRequired(true), ) orgsField = field.StringSliceField( "orgs", @@ -31,13 +37,18 @@ var ( "app-id", field.WithDisplayName("GitHub App ID"), field.WithDescription("The GitHub App to connect to."), + field.WithRequired(true), ) - appPrivateKeyPath = field.StringField( + appPrivateKeyPath = field.FileUploadField( "app-privatekey-path", + []string{".pem"}, field.WithDisplayName("GitHub App private key (.pem)"), field.WithDescription("Path to private key that is used to connect to the GitHub App"), + field.WithIsSecret(true), + field.WithRequired(true), ) + syncSecrets = field.BoolField( "sync-secrets", field.WithDisplayName("Sync secrets"), @@ -48,16 +59,12 @@ var ( field.WithDisplayName("Omit syncing archived repositories"), field.WithDescription("Whether to skip syncing archived repositories or not"), ) - fieldRelationships = []field.SchemaFieldRelationship{ - field.FieldsMutuallyExclusive( - accessTokenField, - appPrivateKeyPath, - ), - field.FieldsRequiredTogether( - appPrivateKeyPath, - appIDField, - ), - } + orgField = field.StringField( + "org", + field.WithDisplayName("Github App Organization"), + field.WithDescription("Organization of your github app"), + field.WithRequired(true), + ) ) //go:generate go run ./gen @@ -71,9 +78,25 @@ var Config = field.NewConfiguration( omitArchivedRepositories, appIDField, appPrivateKeyPath, + orgField, }, - field.WithConstraints(fieldRelationships...), field.WithConnectorDisplayName("GitHub v2"), field.WithHelpUrl("/docs/baton/github-v2"), field.WithIconUrl("/static/app-icons/github.svg"), + field.WithFieldGroups([]field.SchemaFieldGroup{ + { + Name: GithubPersonalAccessTokenGroup, + DisplayName: "Personal access token", + HelpText: "Use a personal access token for authentication.", + Fields: []field.SchemaField{accessTokenField, orgsField, omitArchivedRepositories}, + Default: true, + }, + { + Name: GithubAppGroup, + DisplayName: "GitHub app", + HelpText: "Use a github app for authentication", + Fields: []field.SchemaField{appIDField, appPrivateKeyPath, orgField, syncSecrets, omitArchivedRepositories}, + Default: false, + }, + }), ) diff --git a/pkg/connector/api_token.go b/pkg/connector/api_token.go index 19b48018..72be2f4f 100644 --- a/pkg/connector/api_token.go +++ b/pkg/connector/api_token.go @@ -6,7 +6,6 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" ) @@ -55,72 +54,78 @@ func (o *apiTokenResourceType) ResourceType(_ context.Context) *v2.ResourceType return o.resourceType } -func (o *apiTokenResourceType) Entitlements(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (o *apiTokenResourceType) Entitlements(ctx context.Context, resource *v2.Resource, opts resourceSdk.SyncOpAttrs) ([]*v2.Entitlement, *resourceSdk.SyncOpResults, error) { // API Token secrets do not have entitlements - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } -func (o *apiTokenResourceType) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { +func (o *apiTokenResourceType) Grants(ctx context.Context, resource *v2.Resource, opts resourceSdk.SyncOpAttrs) ([]*v2.Grant, *resourceSdk.SyncOpResults, error) { // API Token secrets do not have grants - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } func (o *apiTokenResourceType) List( ctx context.Context, parentID *v2.ResourceId, - pToken *pagination.Token, -) ([]*v2.Resource, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Resource, *resourceSdk.SyncOpResults, error) { var annotations annotations.Annotations if parentID == nil { - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - bag, page, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: resourceTypeApiToken.Id}) + bag, page, err := parsePageToken(opts.PageToken.Token, &v2.ResourceId{ResourceType: resourceTypeApiToken.Id}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - orgName, err := o.orgCache.GetOrgName(ctx, parentID) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } tokens, resp, err := o.client.Organizations.ListFineGrainedPersonalAccessTokens(ctx, orgName, &github.ListFineGrainedPATOptions{ ListOptions: github.ListOptions{ Page: page, - PerPage: pToken.Size, + PerPage: opts.PageToken.Size, }, }) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list fine-grained personal access tokens") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list fine-grained personal access tokens") } restApiRateLimit, err := extractRateLimitData(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } annotations.WithRateLimiting(restApiRateLimit) nextPage, _, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } pageToken, err := bag.NextToken(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } var rv []*v2.Resource for _, t := range tokens { resource, err := apiTokenResource(ctx, t) if err != nil { - return nil, pageToken, annotations, err + return nil, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: annotations, + }, err } rv = append(rv, resource) } - return rv, pageToken, annotations, nil + return rv, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: annotations, + }, nil } func apiTokenBuilder(client *github.Client, hasSAMLEnabled *bool, orgCache *orgNameCache) *apiTokenResourceType { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 45679dc0..559e6d23 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -17,7 +17,9 @@ import ( "github.com/conductorone/baton-github/pkg/customclient" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" + "github.com/conductorone/baton-sdk/pkg/cli" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" + "github.com/conductorone/baton-sdk/pkg/field" "github.com/conductorone/baton-sdk/pkg/uhttp" jwtv5 "github.com/golang-jwt/jwt/v5" "github.com/google/go-github/v69/github" @@ -105,8 +107,8 @@ type GitHub struct { enterprises []string } -func (gh *GitHub) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncer { - resourceSyncers := []connectorbuilder.ResourceSyncer{ +func (gh *GitHub) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncerV2 { + resourceSyncers := []connectorbuilder.ResourceSyncerV2{ orgBuilder(gh.client, gh.appClient, gh.orgCache, gh.orgs, gh.syncSecrets), teamBuilder(gh.client, gh.orgCache), userBuilder(gh.client, gh.hasSAMLEnabled, gh.graphqlClient, gh.orgCache, gh.orgs), @@ -249,66 +251,104 @@ func newGitHubClient(ctx context.Context, instanceURL string, ts oauth2.TokenSou return gc, nil } -// New returns the GitHub connector configured to sync against the instance URL. -func New(ctx context.Context, ghc *cfg.Github, appKey string) (*GitHub, error) { - jwttoken, patToken, err := getClientToken(ghc, appKey) - if err != nil { - return nil, err +func NewLambdaConnector(ctx context.Context, ghc *cfg.Github, cliOpts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error) { + if err := field.Validate(cfg.Config, ghc); err != nil { + return nil, nil, err } var ( - appClient *github.Client - ts = oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: patToken}, - ) + group = cliOpts.SelectedAuthMethod + cb *GitHub + err error ) - if jwttoken != "" { - if len(ghc.Orgs) != 1 { - return nil, fmt.Errorf("github-connector: only one org should be specified") - } - - appClient, err = newGitHubClient(ctx, - ghc.InstanceUrl, - oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: jwttoken}, - ), - ) + if group == cfg.GithubAppGroup { + cb, err = newWithGithubApp(ctx, ghc) if err != nil { - return nil, err - } - installation, err := findInstallation(ctx, appClient, ghc.Orgs[0]) - if err != nil { - return nil, err + return nil, nil, err } + return cb, nil, nil + } - token, err := getInstallationToken(ctx, appClient, installation.GetID()) - if err != nil { - return nil, err - } + cb, err = newWithGithubPAT(ctx, ghc) + if err != nil { + return nil, nil, err + } + return cb, nil, nil +} - ts = oauth2.ReuseTokenSource( - &oauth2.Token{ - AccessToken: token.GetToken(), - Expiry: token.GetExpiresAt().Time, - }, - &appTokenRefresher{ - ctx: ctx, - instanceURL: ghc.InstanceUrl, - installationID: installation.GetID(), - jwtTokenSource: oauth2.ReuseTokenSource( - &oauth2.Token{ - AccessToken: jwttoken, - Expiry: time.Now().Add(jwtExpiryTime), - }, - &appJWTTokenRefresher{ - appID: ghc.AppId, - privateKey: appKey, - }, - ), - }, - ) +func newWithGithubPAT(ctx context.Context, ghc *cfg.Github) (*GitHub, error) { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ghc.Token}, + ) + ghClient, err := newGitHubClient(ctx, ghc.InstanceUrl, ts) + if err != nil { + return nil, err + } + graphqlClient, err := newGitHubGraphqlClient(ctx, ghc.InstanceUrl, ts) + if err != nil { + return nil, err + } + return &GitHub{ + client: ghClient, + customClient: customclient.New(ghClient), + instanceURL: ghc.InstanceUrl, + orgs: ghc.Orgs, + enterprises: ghc.Enterprises, + graphqlClient: graphqlClient, + orgCache: newOrgNameCache(ghClient), + syncSecrets: ghc.SyncSecrets, + omitArchivedRepositories: ghc.OmitArchivedRepositories, + }, nil +} + +func newWithGithubApp(ctx context.Context, ghc *cfg.Github) (*GitHub, error) { + jwttoken, err := getJWTToken(ghc.AppId, string(ghc.AppPrivatekeyPath)) + if err != nil { + return nil, err + } + + appClient, err := newGitHubClient(ctx, + ghc.InstanceUrl, + oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: jwttoken}, + ), + ) + + if err != nil { + return nil, err + } + installation, err := findInstallation(ctx, appClient, ghc.Org) + if err != nil { + return nil, err } + token, err := getInstallationToken(ctx, appClient, installation.GetID()) + if err != nil { + return nil, err + } + + ts := oauth2.ReuseTokenSource( + &oauth2.Token{ + AccessToken: token.GetToken(), + Expiry: token.GetExpiresAt().Time, + }, + &appTokenRefresher{ + ctx: ctx, + instanceURL: ghc.InstanceUrl, + installationID: installation.GetID(), + jwtTokenSource: oauth2.ReuseTokenSource( + &oauth2.Token{ + AccessToken: jwttoken, + Expiry: time.Now().Add(jwtExpiryTime), + }, + &appJWTTokenRefresher{ + appID: ghc.AppId, + privateKey: string(ghc.AppPrivatekeyPath), + }, + ), + }, + ) + ghClient, err := newGitHubClient(ctx, ghc.InstanceUrl, ts) if err != nil { return nil, err @@ -323,7 +363,7 @@ func New(ctx context.Context, ghc *cfg.Github, appKey string) (*GitHub, error) { appClient: appClient, customClient: customclient.New(ghClient), instanceURL: ghc.InstanceUrl, - orgs: ghc.Orgs, + orgs: []string{ghc.Org}, enterprises: ghc.Enterprises, graphqlClient: graphqlClient, orgCache: newOrgNameCache(ghClient), @@ -381,21 +421,6 @@ func loadPrivateKeyFromString(p string) (*rsa.PrivateKey, error) { return x509.ParsePKCS1PrivateKey(block.Bytes) } -// getClientToken returns -// 1. fine-grained personal access tokens if any. -// 2. JWT token if using github app. -func getClientToken(ghc *cfg.Github, privateKey string) (string, string, error) { - if ghc.Token != "" { - return "", ghc.Token, nil - } - - token, err := getJWTToken(ghc.AppId, privateKey) - if err != nil { - return "", "", err - } - return token, "", nil -} - func getJWTToken(appID string, privateKey string) (string, error) { key, err := loadPrivateKeyFromString(privateKey) if err != nil { diff --git a/pkg/connector/enterprise_role.go b/pkg/connector/enterprise_role.go index 81a34061..172af02d 100644 --- a/pkg/connector/enterprise_role.go +++ b/pkg/connector/enterprise_role.go @@ -8,8 +8,6 @@ import ( "github.com/conductorone/baton-github/pkg/customclient" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" "github.com/conductorone/baton-sdk/pkg/types/grant" resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" @@ -82,12 +80,12 @@ func (o *enterpriseRoleResourceType) fillCache(ctx context.Context) error { func (o *enterpriseRoleResourceType) List( ctx context.Context, parentID *v2.ResourceId, - pToken *pagination.Token, -) ([]*v2.Resource, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Resource, *resourceSdk.SyncOpResults, error) { var ret []*v2.Resource cache, err := o.getRoleUsersCache(ctx) if err != nil { - return nil, "", nil, fmt.Errorf("baton-github: error getting user roles cache: %w", err) + return nil, nil, fmt.Errorf("baton-github: error getting user roles cache: %w", err) } for roleId := range cache { @@ -101,19 +99,19 @@ func (o *enterpriseRoleResourceType) List( []resourceSdk.RoleTraitOption{}, ) if err != nil { - return nil, "", nil, fmt.Errorf("baton-github: error creating role resource for %s in enterprise %s: %w", roleName, enterprise, err) + return nil, nil, fmt.Errorf("baton-github: error creating role resource for %s in enterprise %s: %w", roleName, enterprise, err) } ret = append(ret, roleResource) } - return ret, "", nil, nil + return ret, &resourceSdk.SyncOpResults{}, nil } func (o *enterpriseRoleResourceType) Entitlements( _ context.Context, resource *v2.Resource, - _ *pagination.Token, -) ([]*v2.Entitlement, string, annotations.Annotations, error) { + _ resourceSdk.SyncOpAttrs, +) ([]*v2.Entitlement, *resourceSdk.SyncOpResults, error) { rv := []*v2.Entitlement{} rv = append(rv, entitlement.NewAssignmentEntitlement(resource, "assigned", entitlement.WithDisplayName(resource.DisplayName), @@ -124,29 +122,29 @@ func (o *enterpriseRoleResourceType) Entitlements( entitlement.WithGrantableTo(resourceTypeUser), )) - return rv, "", nil, nil + return rv, &resourceSdk.SyncOpResults{}, nil } func (o *enterpriseRoleResourceType) Grants( ctx context.Context, resource *v2.Resource, - pToken *pagination.Token, -) ([]*v2.Grant, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Grant, *resourceSdk.SyncOpResults, error) { cache, err := o.getRoleUsersCache(ctx) if err != nil { - return nil, "", nil, fmt.Errorf("baton-github: error getting user roles cache: %w", err) + return nil, nil, fmt.Errorf("baton-github: error getting user roles cache: %w", err) } ret := []*v2.Grant{} for _, userLogin := range cache[resource.Id.Resource] { user, resp, err := o.client.Users.Get(ctx, userLogin) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, fmt.Sprintf("baton-github: failed to get user %s", userLogin)) + return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("baton-github: failed to get user %s", userLogin)) } principalId, err := resourceSdk.NewResourceID(resourceTypeUser, *user.ID) if err != nil { - return nil, "", nil, fmt.Errorf("baton-github: error creating resource ID for user %s: %w", userLogin, err) + return nil, nil, fmt.Errorf("baton-github: error creating resource ID for user %s: %w", userLogin, err) } ret = append(ret, grant.NewGrant( @@ -156,7 +154,7 @@ func (o *enterpriseRoleResourceType) Grants( )) } - return ret, "", nil, nil + return ret, &resourceSdk.SyncOpResults{}, nil } func enterpriseRoleBuilder(client *github.Client, customClient *customclient.Client, enterprises []string) *enterpriseRoleResourceType { diff --git a/pkg/connector/helpers.go b/pkg/connector/helpers.go index 670abaa6..d144862e 100644 --- a/pkg/connector/helpers.go +++ b/pkg/connector/helpers.go @@ -6,11 +6,12 @@ import ( "net/http" "strconv" "strings" - "sync" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/pagination" + "github.com/conductorone/baton-sdk/pkg/session" + "github.com/conductorone/baton-sdk/pkg/types/sessions" "github.com/conductorone/baton-sdk/pkg/uhttp" "github.com/google/go-github/v69/github" "github.com/shurcooL/githubv4" @@ -27,45 +28,48 @@ func titleCase(s string) string { } type orgNameCache struct { - sync.RWMutex - c *github.Client - orgNames map[string]string + c *github.Client } -func (o *orgNameCache) GetOrgName(ctx context.Context, orgID *v2.ResourceId) (string, error) { - o.RLock() - if orgName, ok := o.orgNames[orgID.Resource]; ok { - o.RUnlock() - return orgName, nil +func (o *orgNameCache) GetOrgName(ctx context.Context, ss sessions.SessionStore, orgID *v2.ResourceId) (string, error) { + orgName, found, err := session.GetJSON[string](ctx, ss, orgID.Resource) + if err != nil { + return "", err } - o.RUnlock() - - o.Lock() - defer o.Unlock() - if orgName, ok := o.orgNames[orgID.Resource]; ok { + if found { return orgName, nil } - oID, err := strconv.ParseInt(orgID.Resource, 10, 64) + login, err := o.GetOrgNameFromRemoteServer(ctx, orgID.Resource) if err != nil { return "", err } - org, _, err := o.c.Organizations.GetByID(ctx, oID) + err = session.SetJSON(ctx, ss, orgID.Resource, login) if err != nil { return "", err } - o.orgNames[orgID.Resource] = org.GetLogin() + return login, nil +} + +func (o *orgNameCache) GetOrgNameFromRemoteServer(ctx context.Context, rID string) (string, error) { + oID, err := strconv.ParseInt(rID, 10, 64) + if err != nil { + return "", err + } + org, _, err := o.c.Organizations.GetByID(ctx, oID) + if err != nil { + return "", err + } return org.GetLogin(), nil } func newOrgNameCache(c *github.Client) *orgNameCache { return &orgNameCache{ - c: c, - orgNames: make(map[string]string), + c: c, } } diff --git a/pkg/connector/invitation.go b/pkg/connector/invitation.go index 168f4d1b..c74f6d38 100644 --- a/pkg/connector/invitation.go +++ b/pkg/connector/invitation.go @@ -8,8 +8,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" - "github.com/conductorone/baton-sdk/pkg/pagination" - "github.com/conductorone/baton-sdk/pkg/types/resource" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" ) @@ -19,18 +18,18 @@ func invitationToUserResource(invitation *github.Invitation) (*v2.Resource, erro login = invitation.GetEmail() } - ret, err := resource.NewUserResource( + ret, err := resourceSdk.NewUserResource( login, resourceTypeInvitation, invitation.GetID(), - []resource.UserTraitOption{ - resource.WithEmail(invitation.GetEmail(), true), - resource.WithUserProfile(map[string]interface{}{ + []resourceSdk.UserTraitOption{ + resourceSdk.WithEmail(invitation.GetEmail(), true), + resourceSdk.WithUserProfile(map[string]interface{}{ "login": login, "inviter": invitation.GetInviter().GetLogin(), }), - resource.WithStatus(v2.UserTrait_Status_STATUS_UNSPECIFIED), - resource.WithUserLogin(login), + resourceSdk.WithStatus(v2.UserTrait_Status_STATUS_UNSPECIFIED), + resourceSdk.WithUserLogin(login), }, ) if err != nil { @@ -45,71 +44,72 @@ type invitationResourceType struct { orgs []string } -var _ connectorbuilder.AccountManager = &invitationResourceType{} - func (i *invitationResourceType) ResourceType(_ context.Context) *v2.ResourceType { return resourceTypeInvitation } -func (i *invitationResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (i *invitationResourceType) List(ctx context.Context, parentID *v2.ResourceId, opts resourceSdk.SyncOpAttrs) ([]*v2.Resource, *resourceSdk.SyncOpResults, error) { var annotations annotations.Annotations if parentID == nil { - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - bag, page, err := parsePageToken(pt.Token, &v2.ResourceId{ResourceType: resourceTypeInvitation.Id}) + bag, page, err := parsePageToken(opts.PageToken.Token, &v2.ResourceId{ResourceType: resourceTypeInvitation.Id}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - orgName, err := i.orgCache.GetOrgName(ctx, parentID) + orgName, err := i.orgCache.GetOrgName(ctx, opts.Session, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } invitations, resp, err := i.client.Organizations.ListPendingOrgInvitations(ctx, orgName, &github.ListOptions{ Page: page, - PerPage: pt.Size, + PerPage: opts.PageToken.Size, }) if err != nil { if isNotFoundError(resp) { - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list pending org invitations") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list pending org invitations") } restApiRateLimit, err := extractRateLimitData(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } nextPage, _, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } pageToken, err := bag.NextToken(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } invitationResources := make([]*v2.Resource, 0, len(invitations)) for _, invitation := range invitations { ir, err := invitationToUserResource(invitation) if err != nil { - return nil, "", nil, err + return nil, nil, err } invitationResources = append(invitationResources, ir) } annotations.WithRateLimiting(restApiRateLimit) - return invitationResources, pageToken, annotations, nil + return invitationResources, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: annotations, + }, nil } -func (i *invitationResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - return nil, "", nil, nil +func (i *invitationResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ resourceSdk.SyncOpAttrs) ([]*v2.Entitlement, *resourceSdk.SyncOpResults, error) { + return nil, &resourceSdk.SyncOpResults{}, nil } -func (i *invitationResourceType) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - return nil, "", nil, nil +func (i *invitationResourceType) Grants(ctx context.Context, resource *v2.Resource, opts resourceSdk.SyncOpAttrs) ([]*v2.Grant, *resourceSdk.SyncOpResults, error) { + return nil, &resourceSdk.SyncOpResults{}, nil } func (i *invitationResourceType) CreateAccountCapabilityDetails(ctx context.Context) (*v2.CredentialDetailsAccountProvisioning, annotations.Annotations, error) { diff --git a/pkg/connector/org.go b/pkg/connector/org.go index 7164d022..59f1fb7b 100644 --- a/pkg/connector/org.go +++ b/pkg/connector/org.go @@ -12,7 +12,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" "github.com/conductorone/baton-sdk/pkg/types/grant" - "github.com/conductorone/baton-sdk/pkg/types/resource" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/conductorone/baton-sdk/pkg/uhttp" "github.com/google/go-github/v69/github" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" @@ -60,12 +60,12 @@ func organizationResource( annotations = append(annotations, &v2.ChildResourceType{ResourceTypeId: resourceTypeApiToken.Id}) } - return resource.NewResource( + return resourceSdk.NewResource( org.GetLogin(), resourceTypeOrg, org.GetID(), - resource.WithParentResourceID(parentResourceID), - resource.WithAnnotation( + resourceSdk.WithParentResourceID(parentResourceID), + resourceSdk.WithAnnotation( annotations..., ), ) @@ -78,41 +78,44 @@ func (o *orgResourceType) ResourceType(_ context.Context) *v2.ResourceType { func (o *orgResourceType) List( ctx context.Context, parentResourceID *v2.ResourceId, - pToken *pagination.Token, -) ([]*v2.Resource, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Resource, *resourceSdk.SyncOpResults, error) { if o.appClient != nil { orgResource, pageToken, anno, err := o.listOrganizationsFromAppInstallations(ctx, parentResourceID) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return []*v2.Resource{orgResource}, pageToken, anno, nil + return []*v2.Resource{orgResource}, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: anno, + }, nil } l := ctxzap.Extract(ctx) - bag, page, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: resourceTypeOrg.Id}) + bag, page, err := parsePageToken(opts.PageToken.Token, &v2.ResourceId{ResourceType: resourceTypeOrg.Id}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - opts := &github.ListOptions{ + listOpts := &github.ListOptions{ Page: page, PerPage: maxPageSize, } - orgs, resp, err := o.client.Organizations.List(ctx, "", opts) + orgs, resp, err := o.client.Organizations.List(ctx, "", listOpts) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to fetch organizations") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to fetch organizations") } nextPage, reqAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } pageToken, err := bag.NextToken(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ret []*v2.Resource @@ -126,7 +129,7 @@ func (o *orgResourceType) List( l.Warn("insufficient access to list org membership, skipping org", zap.String("org", org.GetLogin())) continue } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to get org membership") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to get org membership") } // Only sync orgs that we are an admin for @@ -136,20 +139,23 @@ func (o *orgResourceType) List( orgResource, err := organizationResource(ctx, org, parentResourceID, o.syncSecrets) if err != nil { - return nil, "", nil, err + return nil, nil, err } ret = append(ret, orgResource) } - return ret, pageToken, reqAnnos, nil + return ret, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } func (o *orgResourceType) Entitlements( _ context.Context, resource *v2.Resource, - _ *pagination.Token, -) ([]*v2.Entitlement, string, annotations.Annotations, error) { + _ resourceSdk.SyncOpAttrs, +) ([]*v2.Entitlement, *resourceSdk.SyncOpResults, error) { rv := make([]*v2.Entitlement, 0, len(orgAccessLevels)) rv = append(rv, entitlement.NewAssignmentEntitlement(resource, orgRoleMember, entitlement.WithDisplayName(fmt.Sprintf("%s Org %s", resource.DisplayName, titleCase(orgRoleMember))), @@ -168,7 +174,7 @@ func (o *orgResourceType) Entitlements( entitlement.WithGrantableTo(resourceTypeUser), )) - return rv, "", nil, nil + return rv, &resourceSdk.SyncOpResults{}, nil } func (o *orgResourceType) orgRoleGrant(roleName string, org *v2.Resource, principalID *v2.ResourceId, userID int64) *v2.Grant { @@ -180,11 +186,11 @@ func (o *orgResourceType) orgRoleGrant(roleName string, org *v2.Resource, princi func (o *orgResourceType) Grants( ctx context.Context, resource *v2.Resource, - pToken *pagination.Token, -) ([]*v2.Grant, string, annotations.Annotations, error) { - bag, page, err := parsePageToken(pToken.Token, resource.Id) + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Grant, *resourceSdk.SyncOpResults, error) { + bag, page, err := parsePageToken(opts.PageToken.Token, resource.Id) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ( @@ -204,40 +210,40 @@ func (o *orgResourceType) Grants( }) case orgRoleAdmin, orgRoleMember: - orgName, err := o.orgCache.GetOrgName(ctx, resource.Id) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, resource.Id) if err != nil { - return nil, "", nil, err + return nil, nil, err } - opts := github.ListMembersOptions{ + listOpts := github.ListMembersOptions{ Role: rId, ListOptions: github.ListOptions{ Page: page, PerPage: maxPageSize, }, } - users, resp, err := o.client.Organizations.ListMembers(ctx, orgName, &opts) + users, resp, err := o.client.Organizations.ListMembers(ctx, orgName, &listOpts) if err != nil { if isNotFoundError(resp) { - return nil, "", nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("org: %s not found", orgName)) + return nil, nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("org: %s not found", orgName)) } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list org members") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list org members") } var nextPage string nextPage, reqAnnos, err = parseResp(resp) if err != nil { - return nil, "", nil, fmt.Errorf("github-connectorv2: failed to parse response: %w", err) + return nil, nil, fmt.Errorf("github-connectorv2: failed to parse response: %w", err) } err = bag.Next(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, user := range users { ur, err := userResource(ctx, user, user.GetEmail(), nil) if err != nil { - return nil, "", nil, err + return nil, nil, err } if rId == orgRoleAdmin { @@ -253,9 +259,12 @@ func (o *orgResourceType) Grants( pageToken, err = bag.Marshal() if err != nil { - return nil, "", nil, err + return nil, nil, err } - return rv, pageToken, reqAnnos, nil + return rv, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } func (o *orgResourceType) Grant(ctx context.Context, principal *v2.Resource, en *v2.Entitlement) (annotations.Annotations, error) { @@ -273,7 +282,7 @@ func (o *orgResourceType) Grant(ctx context.Context, principal *v2.Resource, en adminRoleID := entitlement.NewEntitlementID(en.Resource, orgRoleAdmin) memberRoleID := entitlement.NewEntitlementID(en.Resource, orgRoleMember) - orgName, err := o.orgCache.GetOrgName(ctx, en.Resource.Id) + orgName, err := o.orgCache.GetOrgNameFromRemoteServer(ctx, en.Resource.Id.GetResource()) if err != nil { return nil, err } @@ -365,7 +374,7 @@ func (o *orgResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotati return nil, fmt.Errorf("github-connectorv2: invalid entitlement id: %s", en.Id) } - orgName, err := o.orgCache.GetOrgName(ctx, en.Resource.Id) + orgName, err := o.orgCache.GetOrgNameFromRemoteServer(ctx, en.Resource.Id.GetResource()) if err != nil { return nil, err } diff --git a/pkg/connector/org_role.go b/pkg/connector/org_role.go index b733fc8e..32faf6c6 100644 --- a/pkg/connector/org_role.go +++ b/pkg/connector/org_role.go @@ -12,7 +12,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" "github.com/conductorone/baton-sdk/pkg/types/grant" - "github.com/conductorone/baton-sdk/pkg/types/resource" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" @@ -49,15 +49,15 @@ func orgRoleResource( "description": role.Description, } - return resource.NewRoleResource( + return resourceSdk.NewRoleResource( role.Name, resourceTypeOrgRole, role.ID, - []resource.RoleTraitOption{ - resource.WithRoleProfile(profile), + []resourceSdk.RoleTraitOption{ + resourceSdk.WithRoleProfile(profile), }, - resource.WithParentResourceID(org.Id), - resource.WithAnnotation( + resourceSdk.WithParentResourceID(org.Id), + resourceSdk.WithAnnotation( &v2.V1Identifier{Id: fmt.Sprintf("org_role:%d", role.ID)}, ), ) @@ -70,15 +70,15 @@ func (o *orgRoleResourceType) ResourceType(_ context.Context) *v2.ResourceType { func (o *orgRoleResourceType) List( ctx context.Context, parentID *v2.ResourceId, - pToken *pagination.Token, -) ([]*v2.Resource, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Resource, *resourceSdk.SyncOpResults, error) { if parentID == nil { - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - orgName, err := o.orgCache.GetOrgName(ctx, parentID) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } roles, resp, err := o.client.Organizations.ListRoles(ctx, orgName) @@ -86,9 +86,9 @@ func (o *orgRoleResourceType) List( // Handle permission errors gracefully if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) { // Return empty list with no error to indicate we skipped this resource - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list organization roles") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list organization roles") } var ret []*v2.Resource @@ -99,19 +99,19 @@ func (o *orgRoleResourceType) List( Description: role.GetDescription(), }, &v2.Resource{Id: parentID}) if err != nil { - return nil, "", nil, err + return nil, nil, err } ret = append(ret, roleResource) } - return ret, "", nil, nil + return ret, &resourceSdk.SyncOpResults{}, nil } func (o *orgRoleResourceType) Entitlements( _ context.Context, resource *v2.Resource, - _ *pagination.Token, -) ([]*v2.Entitlement, string, annotations.Annotations, error) { + _ resourceSdk.SyncOpAttrs, +) ([]*v2.Entitlement, *resourceSdk.SyncOpResults, error) { rv := make([]*v2.Entitlement, 0, 1) rv = append(rv, entitlement.NewAssignmentEntitlement(resource, "assigned", entitlement.WithDisplayName(resource.DisplayName), @@ -122,26 +122,26 @@ func (o *orgRoleResourceType) Entitlements( entitlement.WithGrantableTo(resourceTypeUser), )) - return rv, "", nil, nil + return rv, &resourceSdk.SyncOpResults{}, nil } func (o *orgRoleResourceType) Grants( ctx context.Context, resource *v2.Resource, - pToken *pagination.Token, -) ([]*v2.Grant, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Grant, *resourceSdk.SyncOpResults, error) { if resource == nil { - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - bag, page, err := parsePageToken(pToken.Token, resource.Id) + bag, page, err := parsePageToken(opts.PageToken.Token, resource.Id) if err != nil { - return nil, "", nil, err + return nil, nil, err } - orgName, err := o.orgCache.GetOrgName(ctx, resource.ParentResourceId) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, resource.ParentResourceId) if err != nil { - return nil, "", nil, err + return nil, nil, err } var rv []*v2.Grant @@ -149,7 +149,7 @@ func (o *orgRoleResourceType) Grants( roleID, err := strconv.ParseInt(resource.Id.Resource, 10, 64) if err != nil { - return nil, "", nil, fmt.Errorf("invalid role ID: %w", err) + return nil, nil, fmt.Errorf("invalid role ID: %w", err) } switch bag.ResourceTypeID() { @@ -162,37 +162,37 @@ func (o *orgRoleResourceType) Grants( ResourceTypeID: resourceTypeTeam.Id, }) case resourceTypeUser.Id: - opts := &github.ListOptions{ + listOpts := &github.ListOptions{ Page: page, PerPage: maxPageSize, } - users, resp, err := o.client.Organizations.ListUsersAssignedToOrgRole(ctx, orgName, roleID, opts) + users, resp, err := o.client.Organizations.ListUsersAssignedToOrgRole(ctx, orgName, roleID, listOpts) if err != nil { if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) { pageToken, err := bag.NextToken("") if err != nil { - return nil, "", nil, err + return nil, nil, err } - return rv, pageToken, nil, nil + return rv, &resourceSdk.SyncOpResults{NextPageToken: pageToken}, nil } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list users assigned to org role") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list users assigned to org role") } nextPage, respAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } reqAnnos = respAnnos err = bag.Next(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } // Create regular grants for direct user assignments. for _, user := range users { userResource, err := userResource(ctx, user, user.GetEmail(), nil) if err != nil { - return nil, "", nil, err + return nil, nil, err } grant := grant.NewGrant( @@ -207,40 +207,40 @@ func (o *orgRoleResourceType) Grants( rv = append(rv, grant) } case resourceTypeTeam.Id: - opts := &github.ListOptions{ + listOpts := &github.ListOptions{ Page: page, PerPage: maxPageSize, } - teams, resp, err := o.client.Organizations.ListTeamsAssignedToOrgRole(ctx, orgName, roleID, opts) + teams, resp, err := o.client.Organizations.ListTeamsAssignedToOrgRole(ctx, orgName, roleID, listOpts) if err != nil { // Handle permission errors without erroring out. Some customers may not want to give us permissions to get org roles and members. if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) { // Return empty list with no error to indicate we skipped this resource pageToken, err := bag.NextToken("") if err != nil { - return nil, "", nil, err + return nil, nil, err } - return nil, pageToken, nil, nil + return nil, &resourceSdk.SyncOpResults{NextPageToken: pageToken}, nil } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list teams assigned to org role") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list teams assigned to org role") } nextPage, respAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } reqAnnos = respAnnos err = bag.Next(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } // Create expandable grants for teams. To show inherited roles, we need to show the teams that have the role. for _, team := range teams { teamResource, err := teamResource(team, resource.ParentResourceId) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, grant.NewGrant( resource, @@ -260,14 +260,17 @@ func (o *orgRoleResourceType) Grants( )) } default: - return nil, "", nil, fmt.Errorf("unexpected resource type while fetching grants for org role") + return nil, nil, fmt.Errorf("unexpected resource type while fetching grants for org role") } pageToken, err := bag.Marshal() if err != nil { - return nil, "", nil, err + return nil, nil, err } - return rv, pageToken, reqAnnos, nil + return rv, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } func (o *orgRoleResourceType) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) { @@ -287,7 +290,7 @@ func (o *orgRoleResourceType) Grant(ctx context.Context, principal *v2.Resource, return nil, fmt.Errorf("invalid role ID: %w", err) } - orgName, err := o.orgCache.GetOrgName(ctx, entitlement.Resource.ParentResourceId) + orgName, err := o.orgCache.GetOrgNameFromRemoteServer(ctx, entitlement.Resource.ParentResourceId.GetResource()) if err != nil { return nil, fmt.Errorf("failed to get org name: %w", err) } @@ -382,7 +385,7 @@ func (o *orgRoleResourceType) Revoke(ctx context.Context, grant *v2.Grant) (anno return nil, fmt.Errorf("invalid role ID: %w", err) } - orgName, err := o.orgCache.GetOrgName(ctx, entitlement.Resource.ParentResourceId) + orgName, err := o.orgCache.GetOrgNameFromRemoteServer(ctx, entitlement.Resource.ParentResourceId.GetResource()) if err != nil { return nil, fmt.Errorf("failed to get org name: %w", err) } diff --git a/pkg/connector/org_role_test.go b/pkg/connector/org_role_test.go index 3fc34ae4..e5fe6fb9 100644 --- a/pkg/connector/org_role_test.go +++ b/pkg/connector/org_role_test.go @@ -7,6 +7,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/pagination" entitlement2 "github.com/conductorone/baton-sdk/pkg/types/entitlement" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" @@ -54,16 +55,19 @@ func TestOrgRole(t *testing.T) { pToken.Token = token } - nextGrants, nextToken, grantsAnnotations, err := client.Grants(ctx, roleResource, &pToken) + nextGrants, results, err := client.Grants(ctx, roleResource, resourceSdk.SyncOpAttrs{ + PageToken: pToken, + Session: &noOpSessionStore{}, + }) grants = append(grants, nextGrants...) require.Nil(t, err) - test.AssertHasRatelimitAnnotations(t, grantsAnnotations) - if nextToken == "" { + test.AssertHasRatelimitAnnotations(t, results.Annotations) + if results.NextPageToken == "" { break } - err = bag.Unmarshal(nextToken) + err = bag.Unmarshal(results.NextPageToken) if err != nil { t.Error(err) } @@ -94,11 +98,14 @@ func TestOrgRole(t *testing.T) { organization, _ := organizationResource(ctx, githubOrganization, nil, true) // Test List with permission error - resources, nextToken, annotations, err := client.List(ctx, organization.Id, &pagination.Token{}) + resources, results, err := client.List(ctx, organization.Id, resourceSdk.SyncOpAttrs{ + PageToken: pagination.Token{}, + Session: &noOpSessionStore{}, + }) require.Nil(t, err) require.Empty(t, resources) - require.Empty(t, nextToken) - test.AssertHasRatelimitAnnotations(t, annotations) + require.Empty(t, results.NextPageToken) + test.AssertHasRatelimitAnnotations(t, results.Annotations) // Test Grants with permission error role, _ := orgRoleResource(ctx, &OrganizationRole{ @@ -107,11 +114,14 @@ func TestOrgRole(t *testing.T) { Description: orgRole.Description, }, organization) - grants, nextToken, grantsAnnotations, err := client.Grants(ctx, role, &pagination.Token{}) + grants, grantsResults, err := client.Grants(ctx, role, resourceSdk.SyncOpAttrs{ + PageToken: pagination.Token{}, + Session: &noOpSessionStore{}, + }) require.Nil(t, err) require.Empty(t, grants) // The token should contain the initial state for users - require.NotEmpty(t, nextToken) - test.AssertHasRatelimitAnnotations(t, grantsAnnotations) + require.NotEmpty(t, grantsResults.NextPageToken) + test.AssertHasRatelimitAnnotations(t, grantsResults.Annotations) }) } diff --git a/pkg/connector/org_test.go b/pkg/connector/org_test.go index bb1a27da..590f13a0 100644 --- a/pkg/connector/org_test.go +++ b/pkg/connector/org_test.go @@ -9,6 +9,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" ) @@ -37,10 +38,13 @@ func TestOrganization(t *testing.T) { require.Nil(t, err) require.Empty(t, grantAnnotations) - _, nextToken, grantsAnnotations, err := client.Grants(ctx, organization, &pagination.Token{}) + _, results, err := client.Grants(ctx, organization, resourceSdk.SyncOpAttrs{ + PageToken: pagination.Token{}, + Session: &noOpSessionStore{}, + }) require.Nil(t, err) - test.AssertHasRatelimitAnnotations(t, grantsAnnotations) - require.Equal(t, "{\"states\":[{\"type\":\"admin\"}],\"current_state\":{\"type\":\"member\"}}", nextToken) + test.AssertHasRatelimitAnnotations(t, results.Annotations) + require.Equal(t, "{\"states\":[{\"type\":\"admin\"}],\"current_state\":{\"type\":\"member\"}}", results.NextPageToken) grant := v2.Grant{ Entitlement: &entitlement, diff --git a/pkg/connector/repository.go b/pkg/connector/repository.go index 550acd4d..28546fe9 100644 --- a/pkg/connector/repository.go +++ b/pkg/connector/repository.go @@ -12,7 +12,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/entitlement" "github.com/conductorone/baton-sdk/pkg/types/grant" - "github.com/conductorone/baton-sdk/pkg/types/resource" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/conductorone/baton-sdk/pkg/uhttp" "github.com/google/go-github/v69/github" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" @@ -39,15 +39,15 @@ var repoAccessLevels = []string{ // repositoryResource returns a new connector resource for a GitHub repository. func repositoryResource(ctx context.Context, repo *github.Repository, parentResourceID *v2.ResourceId) (*v2.Resource, error) { - ret, err := resource.NewResource( + ret, err := resourceSdk.NewResource( repo.GetName(), resourceTypeRepository, repo.GetID(), - resource.WithAnnotation( + resourceSdk.WithAnnotation( &v2.ExternalLink{Url: repo.GetHTMLURL()}, &v2.V1Identifier{Id: fmt.Sprintf("repo:%d", repo.GetID())}, ), - resource.WithParentResourceID(parentResourceID), + resourceSdk.WithParentResourceID(parentResourceID), ) if err != nil { return nil, err @@ -67,41 +67,41 @@ func (o *repositoryResourceType) ResourceType(_ context.Context) *v2.ResourceTyp return o.resourceType } -func (o *repositoryResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (o *repositoryResourceType) List(ctx context.Context, parentID *v2.ResourceId, opts resourceSdk.SyncOpAttrs) ([]*v2.Resource, *resourceSdk.SyncOpResults, error) { if parentID == nil { - return nil, "", nil, nil + return nil, &resourceSdk.SyncOpResults{}, nil } - bag, page, err := parsePageToken(pt.Token, &v2.ResourceId{ResourceType: resourceTypeRepository.Id}) + bag, page, err := parsePageToken(opts.PageToken.Token, &v2.ResourceId{ResourceType: resourceTypeRepository.Id}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - orgName, err := o.orgCache.GetOrgName(ctx, parentID) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } - opts := &github.RepositoryListByOrgOptions{ + listOpts := &github.RepositoryListByOrgOptions{ ListOptions: github.ListOptions{ Page: page, PerPage: maxPageSize, }, } - repos, resp, err := o.client.Repositories.ListByOrg(ctx, orgName, opts) + repos, resp, err := o.client.Repositories.ListByOrg(ctx, orgName, listOpts) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list repositories") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list repositories") } nextPage, reqAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } pageToken, err := bag.NextToken(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv := make([]*v2.Resource, 0, len(repos)) @@ -111,15 +111,18 @@ func (o *repositoryResourceType) List(ctx context.Context, parentID *v2.Resource } rr, err := repositoryResource(ctx, repo, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, rr) } - return rv, pageToken, reqAnnos, nil + return rv, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } -func (o *repositoryResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (o *repositoryResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ resourceSdk.SyncOpAttrs) ([]*v2.Entitlement, *resourceSdk.SyncOpResults, error) { rv := make([]*v2.Entitlement, 0, len(repoAccessLevels)) for _, level := range repoAccessLevels { rv = append(rv, entitlement.NewPermissionEntitlement(resource, level, @@ -132,23 +135,23 @@ func (o *repositoryResourceType) Entitlements(_ context.Context, resource *v2.Re )) } - return rv, "", nil, nil + return rv, &resourceSdk.SyncOpResults{}, nil } func (o *repositoryResourceType) Grants( ctx context.Context, resource *v2.Resource, - pToken *pagination.Token, -) ([]*v2.Grant, string, annotations.Annotations, error) { + opts resourceSdk.SyncOpAttrs, +) ([]*v2.Grant, *resourceSdk.SyncOpResults, error) { l := ctxzap.Extract(ctx) - bag, page, err := parsePageToken(pToken.Token, resource.Id) + bag, page, err := parsePageToken(opts.PageToken.Token, resource.Id) if err != nil { - return nil, "", nil, err + return nil, nil, err } - orgName, err := o.orgCache.GetOrgName(ctx, resource.ParentResourceId) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, resource.ParentResourceId) if err != nil { - return nil, "", nil, err + return nil, nil, err } var rv []*v2.Grant @@ -165,38 +168,38 @@ func (o *repositoryResourceType) Grants( }) case resourceTypeUser.Id: - opts := &github.ListCollaboratorsOptions{ + listOpts := &github.ListCollaboratorsOptions{ Affiliation: "all", ListOptions: github.ListOptions{ Page: page, PerPage: maxPageSize, }, } - users, resp, err := o.client.Repositories.ListCollaborators(ctx, orgName, resource.DisplayName, opts) + users, resp, err := o.client.Repositories.ListCollaborators(ctx, orgName, resource.DisplayName, listOpts) if err != nil { if resp != nil && resp.StatusCode == http.StatusForbidden { l.Warn("insufficient access to list collaborators", zap.String("repository", resource.DisplayName)) pageToken, err := skipGrantsForResourceType(bag) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return nil, pageToken, nil, nil + return nil, &resourceSdk.SyncOpResults{NextPageToken: pageToken}, nil } if isNotFoundError(resp) { - return nil, "", nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("repo: %s not found", resource.DisplayName)) + return nil, nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("repo: %s not found", resource.DisplayName)) } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list collaborators") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list collaborators") } nextPage, respAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } reqAnnos = respAnnos err = bag.Next(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, user := range users { @@ -207,7 +210,7 @@ func (o *repositoryResourceType) Grants( ur, err := userResource(ctx, user, user.GetEmail(), nil) if err != nil { - return nil, "", nil, err + return nil, nil, err } grant := grant.NewGrant(resource, permission, ur.Id, grant.WithAnnotation(&v2.V1Identifier{ @@ -219,37 +222,37 @@ func (o *repositoryResourceType) Grants( } case resourceTypeTeam.Id: - opts := &github.ListOptions{ + listOpts := &github.ListOptions{ Page: page, PerPage: maxPageSize, } - teams, resp, err := o.client.Repositories.ListTeams(ctx, orgName, resource.DisplayName, opts) + teams, resp, err := o.client.Repositories.ListTeams(ctx, orgName, resource.DisplayName, listOpts) if err != nil { if resp != nil && resp.StatusCode == http.StatusForbidden { l.Warn("insufficient access to list teams", zap.String("repository", resource.DisplayName)) pageToken, err := skipGrantsForResourceType(bag) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return nil, pageToken, nil, nil + return nil, &resourceSdk.SyncOpResults{NextPageToken: pageToken}, nil } if isNotFoundError(resp) { - return nil, "", nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("repo: %s not found", resource.DisplayName)) + return nil, nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("repo: %s not found", resource.DisplayName)) } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list repository teams") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list repository teams") } nextPage, respAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } reqAnnos = respAnnos err = bag.Next(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, team := range teams { @@ -260,7 +263,7 @@ func (o *repositoryResourceType) Grants( tr, err := teamResource(team, resource.ParentResourceId) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, grant.NewGrant(resource, permission, tr.Id, grant.WithAnnotation( @@ -278,15 +281,18 @@ func (o *repositoryResourceType) Grants( } } default: - return nil, "", nil, fmt.Errorf("unexpected resource type while fetching grants for repo") + return nil, nil, fmt.Errorf("unexpected resource type while fetching grants for repo") } pageToken, err := bag.Marshal() if err != nil { - return nil, "", nil, err + return nil, nil, err } - return rv, pageToken, reqAnnos, nil + return rv, &resourceSdk.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } func (o *repositoryResourceType) Grant(ctx context.Context, principal *v2.Resource, en *v2.Entitlement) (annotations.Annotations, error) { diff --git a/pkg/connector/repository_test.go b/pkg/connector/repository_test.go index 5c6ab7bc..e1d213e0 100644 --- a/pkg/connector/repository_test.go +++ b/pkg/connector/repository_test.go @@ -7,6 +7,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/pagination" entitlement2 "github.com/conductorone/baton-sdk/pkg/types/entitlement" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" @@ -49,16 +50,19 @@ func TestRepository(t *testing.T) { pToken.Token = token } - nextGrants, nextToken, grantsAnnotations, err := client.Grants(ctx, repository, &pToken) + nextGrants, results, err := client.Grants(ctx, repository, resourceSdk.SyncOpAttrs{ + PageToken: pToken, + Session: &noOpSessionStore{}, + }) grants = append(grants, nextGrants...) require.Nil(t, err) - test.AssertHasRatelimitAnnotations(t, grantsAnnotations) - if nextToken == "" { + test.AssertHasRatelimitAnnotations(t, results.Annotations) + if results.NextPageToken == "" { break } - err = bag.Unmarshal(nextToken) + err = bag.Unmarshal(results.NextPageToken) if err != nil { t.Error(err) } diff --git a/pkg/connector/session_helper_test.go b/pkg/connector/session_helper_test.go new file mode 100644 index 00000000..3d0e3578 --- /dev/null +++ b/pkg/connector/session_helper_test.go @@ -0,0 +1,39 @@ +package connector + +import ( + "context" + + "github.com/conductorone/baton-sdk/pkg/types/sessions" +) + +// noOpSessionStore always returns nil as response. +type noOpSessionStore struct{} + +func (n *noOpSessionStore) Get(ctx context.Context, key string, opt ...sessions.SessionStoreOption) ([]byte, bool, error) { + return nil, false, nil +} + +func (n *noOpSessionStore) GetMany(ctx context.Context, keys []string, opt ...sessions.SessionStoreOption) (map[string][]byte, []string, error) { + return nil, nil, nil +} + +func (n *noOpSessionStore) Set(ctx context.Context, key string, value []byte, opt ...sessions.SessionStoreOption) error { + return nil +} + +func (n *noOpSessionStore) SetMany(ctx context.Context, values map[string][]byte, opt ...sessions.SessionStoreOption) error { + return nil +} + +func (n *noOpSessionStore) Delete(ctx context.Context, key string, opt ...sessions.SessionStoreOption) error { + return nil +} + +func (n *noOpSessionStore) Clear(ctx context.Context, opt ...sessions.SessionStoreOption) error { + // NOTE: we call this unconditionally for cleanup, so don't throw. + return nil +} + +func (n *noOpSessionStore) GetAll(ctx context.Context, pageToken string, opt ...sessions.SessionStoreOption) (map[string][]byte, string, error) { + return nil, "", nil +} diff --git a/pkg/connector/team.go b/pkg/connector/team.go index 194b0ba2..e6eff4c7 100644 --- a/pkg/connector/team.go +++ b/pkg/connector/team.go @@ -66,52 +66,52 @@ func (o *teamResourceType) ResourceType(_ context.Context) *v2.ResourceType { return o.resourceType } -func (o *teamResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (o *teamResourceType) List(ctx context.Context, parentID *v2.ResourceId, opts rType.SyncOpAttrs) ([]*v2.Resource, *rType.SyncOpResults, error) { if parentID == nil { - return nil, "", nil, nil + return nil, &rType.SyncOpResults{}, nil } - bag, page, err := parsePageToken(pt.Token, &v2.ResourceId{ResourceType: resourceTypeTeam.Id}) + bag, page, err := parsePageToken(opts.PageToken.Token, &v2.ResourceId{ResourceType: resourceTypeTeam.Id}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - opts := &github.ListOptions{ + listOpts := &github.ListOptions{ Page: page, PerPage: maxPageSize, } orgID, err := parseResourceToGitHub(parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } var rv []*v2.Resource - orgName, err := o.orgCache.GetOrgName(ctx, parentID) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } - teams, resp, err := o.client.Teams.ListTeams(ctx, orgName, opts) + teams, resp, err := o.client.Teams.ListTeams(ctx, orgName, listOpts) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list teams") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list teams") } nextPage, reqAnnos, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, team := range teams { fullTeam, resp, err := o.client.Teams.GetTeamByID(ctx, orgID, team.GetID()) //nolint:staticcheck // TODO: migrate to GetTeamBySlug if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to get team details") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to get team details") } tr, err := teamResource(fullTeam, &v2.ResourceId{ResourceType: resourceTypeOrg.Id, Resource: fmt.Sprintf("%d", orgID)}) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, tr) @@ -119,13 +119,16 @@ func (o *teamResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt pageToken, err := bag.NextToken(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } - return rv, pageToken, reqAnnos, nil + return rv, &rType.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } -func (o *teamResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (o *teamResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ rType.SyncOpAttrs) ([]*v2.Entitlement, *rType.SyncOpResults, error) { rv := make([]*v2.Entitlement, 0, len(teamAccessLevels)) for _, level := range teamAccessLevels { rv = append( @@ -145,33 +148,33 @@ func (o *teamResourceType) Entitlements(_ context.Context, resource *v2.Resource ) } - return rv, "", nil, nil + return rv, &rType.SyncOpResults{}, nil } -func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - bag, page, err := parsePageToken(pToken.Token, resource.Id) +func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, opts rType.SyncOpAttrs) ([]*v2.Grant, *rType.SyncOpResults, error) { + bag, page, err := parsePageToken(opts.PageToken.Token, resource.Id) if err != nil { - return nil, "", nil, err + return nil, nil, err } teamTrait, err := rType.GetGroupTrait(resource) if err != nil { - return nil, "", nil, err + return nil, nil, err } orgID, ok := rType.GetProfileInt64Value(teamTrait.Profile, "orgID") if !ok { - return nil, "", nil, fmt.Errorf("error fetching orgID from team profile") + return nil, nil, fmt.Errorf("error fetching orgID from team profile") } org, resp, err := o.client.Organizations.GetByID(ctx, orgID) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to get organization") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to get organization") } githubID, err := parseResourceToGitHub(resource.Id) if err != nil { - return nil, "", nil, err + return nil, nil, err } var ( @@ -189,7 +192,7 @@ func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, pT ResourceTypeID: teamRoleMaintainer, }) case teamRoleMember, teamRoleMaintainer: - opts := github.TeamListTeamMembersOptions{ + listOpts := github.TeamListTeamMembersOptions{ ListOptions: github.ListOptions{ Page: page, PerPage: maxPageSize, @@ -197,29 +200,29 @@ func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, pT Role: rId, } - users, resp, err := o.client.Teams.ListTeamMembersByID(ctx, org.GetID(), githubID, &opts) + users, resp, err := o.client.Teams.ListTeamMembersByID(ctx, org.GetID(), githubID, &listOpts) if err != nil { if isNotFoundError(resp) { - return nil, "", nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("org: %d not found", org.GetID())) + return nil, nil, uhttp.WrapErrors(codes.NotFound, fmt.Sprintf("org: %d not found", org.GetID())) } - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list team members") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list team members") } var nextPage string nextPage, reqAnnos, err = parseResp(resp) if err != nil { - return nil, "", nil, fmt.Errorf("github-connectorv2: failed to parse response: %w", err) + return nil, nil, fmt.Errorf("github-connectorv2: failed to parse response: %w", err) } err = bag.Next(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } for _, user := range users { ur, err := userResource(ctx, user, user.GetEmail(), nil) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, grant.NewGrant(resource, rId, ur.Id, grant.WithAnnotation(&v2.V1Identifier{ @@ -235,9 +238,12 @@ func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, pT pageToken, err = bag.Marshal() if err != nil { - return nil, "", nil, err + return nil, nil, err } - return rv, pageToken, reqAnnos, nil + return rv, &rType.SyncOpResults{ + NextPageToken: pageToken, + Annotations: reqAnnos, + }, nil } func (o *teamResourceType) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) { diff --git a/pkg/connector/team_test.go b/pkg/connector/team_test.go index d739b529..5700f14c 100644 --- a/pkg/connector/team_test.go +++ b/pkg/connector/team_test.go @@ -7,6 +7,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/pagination" entitlement2 "github.com/conductorone/baton-sdk/pkg/types/entitlement" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" @@ -39,10 +40,13 @@ func TestTeam(t *testing.T) { require.Nil(t, err) require.Empty(t, grantAnnotations) - _, nextToken, grantsAnnotations, err := client.Grants(ctx, team, &pagination.Token{}) + _, results, err := client.Grants(ctx, team, resourceSdk.SyncOpAttrs{ + PageToken: pagination.Token{}, + Session: &noOpSessionStore{}, + }) require.Nil(t, err) - test.AssertHasRatelimitAnnotations(t, grantsAnnotations) - require.Equal(t, "{\"states\":[{\"type\":\"member\"}],\"current_state\":{\"type\":\"maintainer\"}}", nextToken) + test.AssertHasRatelimitAnnotations(t, results.Annotations) + require.Equal(t, "{\"states\":[{\"type\":\"member\"}],\"current_state\":{\"type\":\"maintainer\"}}", results.NextPageToken) grant := v2.Grant{ Entitlement: &entitlement, diff --git a/pkg/connector/user.go b/pkg/connector/user.go index 0085d719..93321b73 100644 --- a/pkg/connector/user.go +++ b/pkg/connector/user.go @@ -10,7 +10,6 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/conductorone/baton-sdk/pkg/uhttp" "github.com/google/go-github/v69/github" @@ -101,54 +100,54 @@ func (o *userResourceType) ResourceType(_ context.Context) *v2.ResourceType { return o.resourceType } -func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { +func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, opts resource.SyncOpAttrs) ([]*v2.Resource, *resource.SyncOpResults, error) { l := ctxzap.Extract(ctx) var annotations annotations.Annotations if parentID == nil { - return nil, "", nil, nil + return nil, &resource.SyncOpResults{}, nil } - bag, page, err := parsePageToken(pt.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) + bag, page, err := parsePageToken(opts.PageToken.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { - return nil, "", nil, err + return nil, nil, err } - orgName, err := o.orgCache.GetOrgName(ctx, parentID) + orgName, err := o.orgCache.GetOrgName(ctx, opts.Session, parentID) if err != nil { - return nil, "", nil, err + return nil, nil, err } hasSamlBool, err := o.hasSAML(ctx, orgName) if err != nil { - return nil, "", nil, err + return nil, nil, err } var restApiRateLimit *v2.RateLimitDescription - opts := github.ListMembersOptions{ + listOpts := github.ListMembersOptions{ ListOptions: github.ListOptions{ Page: page, PerPage: maxPageSize, }, } - users, resp, err := o.client.Organizations.ListMembers(ctx, orgName, &opts) + users, resp, err := o.client.Organizations.ListMembers(ctx, orgName, &listOpts) if err != nil { - return nil, "", nil, wrapGitHubError(err, resp, "github-connector: failed to list organization members") + return nil, nil, wrapGitHubError(err, resp, "github-connector: failed to list organization members") } restApiRateLimit, err = extractRateLimitData(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } nextPage, _, err := parseResp(resp) if err != nil { - return nil, "", nil, err + return nil, nil, err } pageToken, err := bag.NextToken(nextPage) if err != nil { - return nil, "", nil, err + return nil, nil, err } q := listUsersQuery{} @@ -157,11 +156,11 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt u, res, err := o.client.Users.GetByID(ctx, user.GetID()) if err != nil { if isRatelimited(res) { - return nil, "", nil, uhttp.WrapErrors(codes.Unavailable, "too many requests", err) + return nil, nil, uhttp.WrapErrors(codes.Unavailable, "too many requests", err) } // This undocumented API can return 404 for some users. If this fails it means we won't get some of their details like email if res == nil || res.StatusCode != http.StatusNotFound { - return nil, "", nil, err + return nil, nil, err } l.Error("error fetching user by id", zap.Error(err), zap.Int64("user_id", user.GetID())) u = user @@ -186,7 +185,7 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt o.hasSAMLEnabled = &samlDisabled hasSamlBool = false } else { - return nil, "", nil, err + return nil, nil, err } } if err == nil && len(q.Organization.SamlIdentityProvider.ExternalIdentities.Edges) == 1 { @@ -214,7 +213,7 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt } ur, err := userResource(ctx, u, userEmail, extraEmails) if err != nil { - return nil, "", nil, err + return nil, nil, err } rv = append(rv, ur) @@ -229,7 +228,10 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt annotations.WithRateLimiting(graphqlRateLimit) } - return rv, pageToken, annotations, nil + return rv, &resource.SyncOpResults{ + NextPageToken: pageToken, + Annotations: annotations, + }, nil } func isEmail(email string) bool { @@ -237,12 +239,12 @@ func isEmail(email string) bool { return err == nil } -func (o *userResourceType) Entitlements(_ context.Context, _ *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - return nil, "", nil, nil +func (o *userResourceType) Entitlements(_ context.Context, _ *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) { + return nil, &resource.SyncOpResults{}, nil } -func (o *userResourceType) Grants(_ context.Context, _ *v2.Resource, _ *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - return nil, "", nil, nil +func (o *userResourceType) Grants(_ context.Context, _ *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) { + return nil, &resource.SyncOpResults{}, nil } func (o *userResourceType) Delete(ctx context.Context, resourceId *v2.ResourceId) (annotations.Annotations, error) { diff --git a/pkg/connector/user_test.go b/pkg/connector/user_test.go index 09e970e7..bf97eb0d 100644 --- a/pkg/connector/user_test.go +++ b/pkg/connector/user_test.go @@ -8,6 +8,7 @@ import ( "github.com/conductorone/baton-github/test" "github.com/conductorone/baton-github/test/mocks" "github.com/conductorone/baton-sdk/pkg/pagination" + resourceSdk "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/google/go-github/v69/github" "github.com/stretchr/testify/require" ) @@ -52,14 +53,17 @@ func TestUsersList(t *testing.T) { []string{organization.DisplayName}, ) - users, nextToken, annotations, err := client.List( + users, results, err := client.List( ctx, organization.Id, - &pagination.Token{}, + resourceSdk.SyncOpAttrs{ + PageToken: pagination.Token{}, + Session: &noOpSessionStore{}, + }, ) require.Nil(t, err) - test.AssertHasRatelimitAnnotations(t, annotations) - require.Equal(t, "", nextToken) + test.AssertHasRatelimitAnnotations(t, results.Annotations) + require.Equal(t, "", results.NextPageToken) require.Len(t, users, 1) require.Equal(t, *githubUser.Login, users[0].Id.Resource) })