Skip to content

Commit ebc811a

Browse files
authored
fix(api): check and grant user before creating pullrequest or comment (#5080)
1 parent b289f6b commit ebc811a

File tree

7 files changed

+140
-80
lines changed

7 files changed

+140
-80
lines changed

engine/api/workflow_queue.go

-13
Original file line numberDiff line numberDiff line change
@@ -499,19 +499,6 @@ func postJobResult(ctx context.Context, dbFunc func(context.Context) *gorp.DbMap
499499
}
500500

501501
go WorkflowSendEvent(context.Background(), tx, store, *proj, reportParent)
502-
503-
if sdk.StatusIsTerminated(run.Status) {
504-
//Start a goroutine to update commit statuses in repositories manager
505-
go func(wRun *sdk.WorkflowRun) {
506-
//The function could be called with nil project so we need to test if project is not nil
507-
if sdk.StatusIsTerminated(wRun.Status) && proj != nil {
508-
wRun.LastExecution = time.Now()
509-
if err := workflow.ResyncCommitStatus(context.Background(), dbFunc(context.Background()), store, *proj, wRun); err != nil {
510-
log.Error(ctx, "workflow.UpdateNodeJobRunStatus> %v", err)
511-
}
512-
}
513-
}(run)
514-
}
515502
}
516503

517504
return report, nil

engine/vcs/bitbucketserver/client_pull_request.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ func (b *bitbucketClient) PullRequestComment(ctx context.Context, repo string, p
9595

9696
path := fmt.Sprintf("/projects/%s/repos/%s/pull-requests/%d/comments", project, slug, prRequest.ID)
9797

98+
canWrite, err := b.UserHasWritePermission(ctx, repo)
99+
if err != nil {
100+
return err
101+
}
102+
if !canWrite {
103+
if err := b.GrantWritePermission(ctx, repo); err != nil {
104+
return err
105+
}
106+
}
98107
return b.do(ctx, "POST", "core", path, nil, values, nil, &options{asUser: true})
99108
}
100109

@@ -104,9 +113,15 @@ func (b *bitbucketClient) PullRequestCreate(ctx context.Context, repo string, pr
104113
return pr, sdk.WithStack(err)
105114
}
106115

107-
if err := b.GrantWritePermission(ctx, repo); err != nil {
116+
canWrite, err := b.UserHasWritePermission(ctx, repo)
117+
if err != nil {
108118
return pr, err
109119
}
120+
if !canWrite {
121+
if err := b.GrantWritePermission(ctx, repo); err != nil {
122+
return pr, err
123+
}
124+
}
110125

111126
request := sdk.BitbucketServerPullRequest{
112127
Title: pr.Title,

engine/vcs/bitbucketserver/client_repos.go

+26
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,32 @@ func (b *bitbucketClient) RepoByFullname(ctx context.Context, fullname string) (
114114
return repo, nil
115115
}
116116

117+
func (b *bitbucketClient) UserHasWritePermission(ctx context.Context, repo string) (bool, error) {
118+
if b.username == "" {
119+
return false, sdk.WrapError(sdk.ErrUserNotFound, "No user found in configuration")
120+
}
121+
122+
project, slug, err := getRepo(repo)
123+
if err != nil {
124+
return false, sdk.WithStack(err)
125+
}
126+
path := fmt.Sprintf("/projects/%s/repos/%s/permissions/users", project, slug)
127+
params := url.Values{}
128+
params.Add("filter", b.username)
129+
130+
var reponse UsersPermissionResponse
131+
if err := b.do(ctx, "GET", "core", path, params, nil, &reponse, nil); err != nil {
132+
return false, sdk.WithStack(err)
133+
}
134+
135+
for _, v := range reponse.Values {
136+
if v.User.Slug == b.username {
137+
return v.Permission == "REPO_WRITE" || v.Permission == "REPO_ADMIN", nil
138+
}
139+
}
140+
return false, nil
141+
}
142+
117143
func (b *bitbucketClient) GrantWritePermission(ctx context.Context, repo string) error {
118144
if b.username == "" {
119145
return nil

engine/vcs/bitbucketserver/types.go

+12
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,15 @@ type PullRequestResponse struct {
248248
NextPageStart int `json:"nextPageStart"`
249249
IsLastPage bool `json:"isLastPage"`
250250
}
251+
252+
type UsersPermissionResponse struct {
253+
Values []UserPermission `json:"values"`
254+
Size int `json:"size"`
255+
NextPageStart int `json:"nextPageStart"`
256+
IsLastPage bool `json:"isLastPage"`
257+
}
258+
259+
type UserPermission struct {
260+
User sdk.BitbucketServerActor `json:"user"`
261+
Permission string `json:"permission"`
262+
}

engine/vcs/github/client_pull_request.go

+20
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ func (g *githubClient) PullRequestComment(ctx context.Context, repo string, prRe
144144
return nil
145145
}
146146

147+
canWrite, err := g.UserHasWritePermission(ctx, repo)
148+
if err != nil {
149+
return err
150+
}
151+
if !canWrite {
152+
if err := g.GrantWritePermission(ctx, repo); err != nil {
153+
return err
154+
}
155+
}
156+
147157
path := fmt.Sprintf("/repos/%s/issues/%d/comments", repo, prReq.ID)
148158
payload := map[string]string{
149159
"body": prReq.Message,
@@ -171,6 +181,16 @@ func (g *githubClient) PullRequestComment(ctx context.Context, repo string, prRe
171181
}
172182

173183
func (g *githubClient) PullRequestCreate(ctx context.Context, repo string, pr sdk.VCSPullRequest) (sdk.VCSPullRequest, error) {
184+
canWrite, err := g.UserHasWritePermission(ctx, repo)
185+
if err != nil {
186+
return sdk.VCSPullRequest{}, nil
187+
}
188+
if !canWrite {
189+
if err := g.GrantWritePermission(ctx, repo); err != nil {
190+
return sdk.VCSPullRequest{}, nil
191+
}
192+
}
193+
174194
path := fmt.Sprintf("/repos/%s/pulls", repo)
175195
payload := map[string]string{
176196
"title": pr.Title,

engine/vcs/github/client_repos.go

+31
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,37 @@ func (g *githubClient) repoByFullname(ctx context.Context, fullname string) (Rep
151151
return repo, nil
152152
}
153153

154+
func (g *githubClient) UserHasWritePermission(ctx context.Context, fullname string) (bool, error) {
155+
owner := strings.SplitN(fullname, "/", 2)[0]
156+
if g.username == "" || owner == g.username {
157+
return false, sdk.WrapError(sdk.ErrUserNotFound, "No user found in configuration")
158+
}
159+
url := "/repos/" + fullname + "/collaborators/" + g.username + "/permission"
160+
k := cache.Key("vcs", "github", "user-write", g.OAuthToken, url)
161+
162+
status, resp, _, err := g.get(ctx, url)
163+
if err != nil {
164+
return false, err
165+
}
166+
if status >= 400 {
167+
return false, sdk.NewError(sdk.ErrUnknownError, errorAPI(resp))
168+
}
169+
var permResp UserPermissionResponse
170+
if status == http.StatusNotModified {
171+
if _, err := g.Cache.Get(k, &permResp); err != nil {
172+
log.Error(ctx, "cannot get from cache %s: %v", k, err)
173+
}
174+
} else {
175+
if err := json.Unmarshal(resp, &permResp); err != nil {
176+
return false, sdk.WrapError(err, "unable to unmarshal: %s", string(resp))
177+
}
178+
if err := g.Cache.SetWithTTL(k, permResp, 61*60); err != nil {
179+
log.Error(ctx, "cannot SetWithTTL: %s: %v", k, err)
180+
}
181+
}
182+
return permResp.Permission == "write" || permResp.Permission == "admin", nil
183+
}
184+
154185
func (g *githubClient) GrantWritePermission(ctx context.Context, fullname string) error {
155186
owner := strings.SplitN(fullname, "/", 2)[0]
156187
if g.username == "" || owner == g.username {

engine/vcs/github/types.go

+35-66
Original file line numberDiff line numberDiff line change
@@ -523,32 +523,34 @@ type ReleaseResponse struct {
523523
UploadURL string `json:"upload_url"`
524524
}
525525

526+
type GithubOwner struct {
527+
Login string `json:"login"`
528+
ID int `json:"id"`
529+
NodeID string `json:"node_id"`
530+
AvatarURL string `json:"avatar_url"`
531+
GravatarID string `json:"gravatar_id"`
532+
URL string `json:"url"`
533+
HTMLURL string `json:"html_url"`
534+
FollowersURL string `json:"followers_url"`
535+
FollowingURL string `json:"following_url"`
536+
GistsURL string `json:"gists_url"`
537+
StarredURL string `json:"starred_url"`
538+
SubscriptionsURL string `json:"subscriptions_url"`
539+
OrganizationsURL string `json:"organizations_url"`
540+
ReposURL string `json:"repos_url"`
541+
EventsURL string `json:"events_url"`
542+
ReceivedEventsURL string `json:"received_events_url"`
543+
Type string `json:"type"`
544+
SiteAdmin bool `json:"site_admin"`
545+
}
546+
526547
// RepositoryInvitation allows users or external services to invite other users to collaborate on a repo. The invited users (or external services on behalf of invited users) can choose to accept or decline the invitations.
527548
type RepositoryInvitation struct {
528549
ID int `json:"id"`
529550
Repository struct {
530-
ID int `json:"id"`
531-
NodeID string `json:"node_id"`
532-
Owner struct {
533-
Login string `json:"login"`
534-
ID int `json:"id"`
535-
NodeID string `json:"node_id"`
536-
AvatarURL string `json:"avatar_url"`
537-
GravatarID string `json:"gravatar_id"`
538-
URL string `json:"url"`
539-
HTMLURL string `json:"html_url"`
540-
FollowersURL string `json:"followers_url"`
541-
FollowingURL string `json:"following_url"`
542-
GistsURL string `json:"gists_url"`
543-
StarredURL string `json:"starred_url"`
544-
SubscriptionsURL string `json:"subscriptions_url"`
545-
OrganizationsURL string `json:"organizations_url"`
546-
ReposURL string `json:"repos_url"`
547-
EventsURL string `json:"events_url"`
548-
ReceivedEventsURL string `json:"received_events_url"`
549-
Type string `json:"type"`
550-
SiteAdmin bool `json:"site_admin"`
551-
} `json:"owner"`
551+
ID int `json:"id"`
552+
NodeID string `json:"node_id"`
553+
Owner GithubOwner `json:"owner"`
552554
Name string `json:"name"`
553555
FullName string `json:"full_name"`
554556
Description string `json:"description"`
@@ -625,50 +627,12 @@ type RepositoryInvitation struct {
625627
SubscribersCount int `json:"subscribers_count"`
626628
NetworkCount int `json:"network_count"`
627629
} `json:"repository"`
628-
Invitee struct {
629-
Login string `json:"login"`
630-
ID int `json:"id"`
631-
NodeID string `json:"node_id"`
632-
AvatarURL string `json:"avatar_url"`
633-
GravatarID string `json:"gravatar_id"`
634-
URL string `json:"url"`
635-
HTMLURL string `json:"html_url"`
636-
FollowersURL string `json:"followers_url"`
637-
FollowingURL string `json:"following_url"`
638-
GistsURL string `json:"gists_url"`
639-
StarredURL string `json:"starred_url"`
640-
SubscriptionsURL string `json:"subscriptions_url"`
641-
OrganizationsURL string `json:"organizations_url"`
642-
ReposURL string `json:"repos_url"`
643-
EventsURL string `json:"events_url"`
644-
ReceivedEventsURL string `json:"received_events_url"`
645-
Type string `json:"type"`
646-
SiteAdmin bool `json:"site_admin"`
647-
} `json:"invitee"`
648-
Inviter struct {
649-
Login string `json:"login"`
650-
ID int `json:"id"`
651-
NodeID string `json:"node_id"`
652-
AvatarURL string `json:"avatar_url"`
653-
GravatarID string `json:"gravatar_id"`
654-
URL string `json:"url"`
655-
HTMLURL string `json:"html_url"`
656-
FollowersURL string `json:"followers_url"`
657-
FollowingURL string `json:"following_url"`
658-
GistsURL string `json:"gists_url"`
659-
StarredURL string `json:"starred_url"`
660-
SubscriptionsURL string `json:"subscriptions_url"`
661-
OrganizationsURL string `json:"organizations_url"`
662-
ReposURL string `json:"repos_url"`
663-
EventsURL string `json:"events_url"`
664-
ReceivedEventsURL string `json:"received_events_url"`
665-
Type string `json:"type"`
666-
SiteAdmin bool `json:"site_admin"`
667-
} `json:"inviter"`
668-
Permissions string `json:"permissions"`
669-
CreatedAt string `json:"created_at"`
670-
URL string `json:"url"`
671-
HTMLURL string `json:"html_url"`
630+
Invitee GithubOwner `json:"invitee"`
631+
Inviter GithubOwner `json:"inviter"`
632+
Permissions string `json:"permissions"`
633+
CreatedAt string `json:"created_at"`
634+
URL string `json:"url"`
635+
HTMLURL string `json:"html_url"`
672636
}
673637

674638
// DiffCommits represent response from github api for a diff between commits
@@ -735,3 +699,8 @@ type Ref struct {
735699
URL string `json:"url"`
736700
} `json:"object"`
737701
}
702+
703+
type UserPermissionResponse struct {
704+
Permission string `json:"permission"`
705+
User GithubOwner `json:"user"`
706+
}

0 commit comments

Comments
 (0)