diff --git a/go.mod b/go.mod index e773ebaa..978b22d3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/conductorone/baton-github-enterprise go 1.25.2 require ( - github.com/conductorone/baton-github v0.3.10 + github.com/conductorone/baton-github v0.4.0 github.com/conductorone/baton-sdk v0.22.1 github.com/ennyjfrick/ruleguard-logfatal v0.0.2 github.com/quasilyte/go-ruleguard/dsl v0.3.23 diff --git a/go.sum b/go.sum index 3564afa1..e3be47c3 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8 github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/conductorone/baton-github v0.3.10 h1:R/eEwIiuyU2jJ3no6vFEb/rFv/dypdVGhfjEffwEgGU= -github.com/conductorone/baton-github v0.3.10/go.mod h1:uMdYU1bf9pJMYysXUwVrNzhRD4XtlJSo7CxEPTd1uqI= +github.com/conductorone/baton-github v0.4.0 h1:Hf72uDLoYrtmhII6BYyRs0qSG/vuhx6Po7QHHs4618E= +github.com/conductorone/baton-github v0.4.0/go.mod h1:SUpbLLRnO+fldsV/ALUd6JjONtUcsPya38psMNBIDCU= github.com/conductorone/baton-sdk v0.22.1 h1:PjJWAunQv7uvpH+3yagSL75Ou/Gja4HTl38fUe/OKes= github.com/conductorone/baton-sdk v0.22.1/go.mod h1:SKm95z4KkQ23Tufo2ys88lVzbwKb0AQEbKee5GE0Lig= github.com/conductorone/dpop v0.2.6 h1:fakwai/Xm2b/fcDUwJN41WtcSI/2UhQOyRIVvnnrrNA= diff --git a/vendor/github.com/conductorone/baton-github/pkg/connector/repository.go b/vendor/github.com/conductorone/baton-github/pkg/connector/repository.go index fe74983e..85762ef7 100644 --- a/vendor/github.com/conductorone/baton-github/pkg/connector/repository.go +++ b/vendor/github.com/conductorone/baton-github/pkg/connector/repository.go @@ -31,6 +31,8 @@ const ( repoPermissionAdmin = "admin" ) +const readConst = "read" + var repoAccessLevels = []string{ repoPermissionPull, repoPermissionTriage, @@ -39,6 +41,24 @@ var repoAccessLevels = []string{ repoPermissionAdmin, } +// roleNameToRepoPermission maps a role returned by the "get repository +// permissions for a user" API (read/triage/write/maintain/admin) to the +// permission vocabulary used by repository entitlements +// (pull/triage/push/maintain/admin). Returns "" for custom repository +// roles it does not recognize. +func roleNameToRepoPermission(roleName string) string { + switch roleName { + case readConst: + return repoPermissionPull + case "write": + return repoPermissionPush + case repoPermissionTriage, repoPermissionMaintain, repoPermissionAdmin: + return roleName + default: + return "" + } +} + // 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 := resourceSdk.NewResource( @@ -389,6 +409,43 @@ func (o *repositoryResourceType) Grant(ctx context.Context, principal *v2.Resour return nil, wrapGitHubError(err, resp, "github-connector: failed to get user") } + collaborator, resp, err := o.client.Repositories.IsCollaborator(ctx, repo.GetOwner().GetLogin(), repo.GetName(), user.GetLogin()) + if err != nil { + return nil, wrapGitHubError(err, resp, "github-connector: failed to check if user is a collaborator") + } + + var replacedGrantID string + if collaborator { + permLevel, resp, err := o.client.Repositories.GetPermissionLevel(ctx, repo.GetOwner().GetLogin(), repo.GetName(), user.GetLogin()) + if err != nil { + return nil, wrapGitHubError(err, resp, "github-connector: failed to get user's repository permission") + } + + prevPermission := roleNameToRepoPermission(permLevel.GetRoleName()) + if prevPermission == "" { + // Custom repository role: fall back to the coarse permission (read/write/admin). + prevPermission = roleNameToRepoPermission(permLevel.GetPermission()) + } + + switch prevPermission { + case permission: + return annotations.New(&v2.GrantAlreadyExists{}), nil + case "": + l.Warn( + "github-connectorv2: unrecognized existing repository role, granting without GrantReplaced annotation", + zap.String("role_name", permLevel.GetRoleName()), + zap.String("permission", permLevel.GetPermission()), + ) + default: + // AddCollaborator overwrites the user's existing role, so report the + // old role's grant as replaced. GitHub permissions are cumulative; + // grants for other implied flags are reconciled at the next sync. + replacedGrantID = grant.NewGrantID(principal, &v2.Entitlement{ + Id: entitlement.NewEntitlementID(en.Resource, prevPermission), + }) + } + } + _, resp, er := o.client.Repositories.AddCollaborator( ctx, repo.GetOwner().GetLogin(), @@ -400,6 +457,10 @@ func (o *repositoryResourceType) Grant(ctx context.Context, principal *v2.Resour if er != nil { return nil, wrapGitHubError(er, resp, "github-connector: failed to add user to repository") } + + if replacedGrantID != "" { + return annotations.New(&v2.GrantReplaced{ReplacedGrantId: replacedGrantID}), nil + } case resourceTypeTeam.Id: team, resp, err := o.client.Teams.GetTeamByID(ctx, org.GetID(), principalID) //nolint:staticcheck,nolintlint // TODO: migrate to GetTeamBySlug if err != nil { @@ -487,7 +548,14 @@ func orgBasePermissionSessionKey(orgID string) string { // getOrgBasePermission fetches the org's default_repository_permission, caching in the session. // Returns "read", "write", "admin", or "none". +// +// An empty/absent field is treated as "none" (fail closed). GitHub only returns +// default_repository_permission to org owners / tokens with admin:org (or the +// GitHub App Organization Administration permission). An omitted field means +// "unknown", not GitHub's create-org default of "read" — assuming "read" would +// invent pull grants for every org member on every repo. func (o *repositoryResourceType) getOrgBasePermission(ctx context.Context, ss sessions.SessionStore, orgName string, orgResourceID *v2.ResourceId) (string, error) { + l := ctxzap.Extract(ctx) key := orgBasePermissionSessionKey(orgResourceID.Resource) cached, found, err := session.GetJSON[string](ctx, ss, key) if err != nil { @@ -504,7 +572,13 @@ func (o *repositoryResourceType) getOrgBasePermission(ctx context.Context, ss se perm := org.GetDefaultRepoPermission() if perm == "" { - perm = "read" // GitHub default + l.Debug( + "baton-github: org default_repository_permission missing or empty; skipping org-member repo expansion (treating as none). "+ + "Grant the credential org-owner visibility (admin:org / Organization Administration) to sync base-permission grants accurately.", + zap.String("org", orgName), + zap.String("org_id", orgResourceID.Resource), + ) + perm = "none" } if err := session.SetJSON(ctx, ss, key, perm); err != nil { @@ -521,7 +595,7 @@ func orgBasePermissionToRepoPermissions(basePerm string) []string { return []string{repoPermissionPull, repoPermissionTriage, repoPermissionPush, repoPermissionMaintain, repoPermissionAdmin} case "write": return []string{repoPermissionPull, repoPermissionTriage, repoPermissionPush} - case "read": + case readConst: return []string{repoPermissionPull} default: return nil diff --git a/vendor/modules.txt b/vendor/modules.txt index fa3a09cb..510a4b4e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -272,7 +272,7 @@ github.com/cockroachdb/swiss # github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 ## explicit; go 1.19 github.com/cockroachdb/tokenbucket -# github.com/conductorone/baton-github v0.3.10 +# github.com/conductorone/baton-github v0.4.0 ## explicit; go 1.25.2 github.com/conductorone/baton-github/pkg/config github.com/conductorone/baton-github/pkg/connector