From 9ef5db47e5d594fa4780041243909d5c72c97f4d Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Thu, 11 Jun 2026 15:07:11 -0400 Subject: [PATCH 1/2] add GrantReplaced annotation for grant --- pkg/connector/repository.go | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/pkg/connector/repository.go b/pkg/connector/repository.go index fe74983e..6f63fe82 100644 --- a/pkg/connector/repository.go +++ b/pkg/connector/repository.go @@ -39,6 +39,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 "read": + 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 +407,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 +455,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 { From 4c75fb085fd2c93ceda44edcc272a34c31b26dcb Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Fri, 12 Jun 2026 14:38:55 -0400 Subject: [PATCH 2/2] fix lint --- pkg/connector/repository.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/connector/repository.go b/pkg/connector/repository.go index 6f63fe82..841aedd2 100644 --- a/pkg/connector/repository.go +++ b/pkg/connector/repository.go @@ -31,6 +31,8 @@ const ( repoPermissionAdmin = "admin" ) +const readConst = "read" + var repoAccessLevels = []string{ repoPermissionPull, repoPermissionTriage, @@ -46,7 +48,7 @@ var repoAccessLevels = []string{ // roles it does not recognize. func roleNameToRepoPermission(roleName string) string { switch roleName { - case "read": + case readConst: return repoPermissionPull case "write": return repoPermissionPush @@ -563,7 +565,7 @@ func (o *repositoryResourceType) getOrgBasePermission(ctx context.Context, ss se perm := org.GetDefaultRepoPermission() if perm == "" { - perm = "read" // GitHub default + perm = readConst // GitHub default } if err := session.SetJSON(ctx, ss, key, perm); err != nil { @@ -580,7 +582,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