Merge pull request 'Check for repo write permissions -- needed to create labels' (#5) from label-again into main

Reviewed-on: https://git.ts.mattnite.net/mattnite/forgejo-tickets/pulls/5
This commit is contained in:
Matthew Knight 2026-02-16 18:32:00 +00:00
commit 0df41e08a0
3 changed files with 80 additions and 3 deletions

View File

@ -429,6 +429,42 @@ func (c *Client) CreateLabel(owner, repo, labelName, color string) (*Label, erro
return &label, nil return &label, nil
} }
// CheckRepoPermission verifies the bot user has write (push) access to the repo.
func (c *Client) CheckRepoPermission(owner, repo string) error {
reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s", c.baseURL, owner, repo)
httpReq, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return err
}
httpReq.Header.Set("Authorization", "token "+c.apiToken)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("forgejo API request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("forgejo API returned %d: %s", resp.StatusCode, string(respBody))
}
var result struct {
Permissions struct {
Admin bool `json:"admin"`
Push bool `json:"push"`
Pull bool `json:"pull"`
} `json:"permissions"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if !result.Permissions.Push {
return fmt.Errorf("bot user does not have write access to %s/%s — add it as a collaborator with Write permission", owner, repo)
}
return nil
}
// GetOrCreateLabel looks up a label by name, creating it if it doesn't exist. // GetOrCreateLabel looks up a label by name, creating it if it doesn't exist.
func (c *Client) GetOrCreateLabel(owner, repo, labelName, color string) (*Label, error) { func (c *Client) GetOrCreateLabel(owner, repo, labelName, color string) (*Label, error) {
label, err := c.GetLabel(owner, repo, labelName) label, err := c.GetLabel(owner, repo, labelName)

View File

@ -51,6 +51,19 @@ func (h *RepoHandler) Create(c *gin.Context) {
return return
} }
// Verify the bot user has write access to the Forgejo repo
if err := h.deps.ForgejoClient.CheckRepoPermission(forgejoOwner, forgejoRepo); err != nil {
log.Error().Err(err).Msg("repo permission check failed")
h.deps.Renderer.Render(c.Writer, c.Request, "admin/repos/new", map[string]interface{}{
"Error": "Forgejo repo check failed: " + err.Error(),
"Name": name,
"Slug": slug,
"ForgejoOwner": forgejoOwner,
"ForgejoRepo": forgejoRepo,
})
return
}
webhookSecret, err := forgejo.GenerateWebhookSecret() webhookSecret, err := forgejo.GenerateWebhookSecret()
if err != nil { if err != nil {
log.Error().Err(err).Msg("generate webhook secret error") log.Error().Err(err).Msg("generate webhook secret error")
@ -105,10 +118,17 @@ func (h *RepoHandler) EditForm(c *gin.Context) {
return return
} }
// Check bot write access to the Forgejo repo
var repoPermErr string
if err := h.deps.ForgejoClient.CheckRepoPermission(repo.ForgejoOwner, repo.ForgejoRepo); err != nil {
repoPermErr = err.Error()
}
h.deps.Renderer.Render(c.Writer, c.Request, "admin/repos/edit", map[string]interface{}{ h.deps.Renderer.Render(c.Writer, c.Request, "admin/repos/edit", map[string]interface{}{
"Repo": repo, "Repo": repo,
"BaseURL": h.deps.Config.BaseURL, "BaseURL": h.deps.Config.BaseURL,
"ForgejoURL": h.deps.Config.ForgejoURL, "ForgejoURL": h.deps.Config.ForgejoURL,
"RepoPermErr": repoPermErr,
}) })
} }

View File

@ -9,6 +9,27 @@
<h1 class="text-2xl font-bold text-gray-900 mb-6">Edit Repo</h1> <h1 class="text-2xl font-bold text-gray-900 mb-6">Edit Repo</h1>
{{if .RepoPermErr}}
<div class="mb-6 rounded-md bg-red-50 p-4 ring-1 ring-red-200">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<p class="text-sm font-semibold text-red-800">Bot write access missing</p>
</div>
<p class="mt-1 text-sm text-red-700">{{.RepoPermErr}}</p>
</div>
{{else}}
<div class="mb-6 rounded-md bg-green-50 p-4 ring-1 ring-green-200">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-sm font-semibold text-green-800">Bot write access verified</p>
</div>
</div>
{{end}}
{{if .Repo.WebhookVerified}} {{if .Repo.WebhookVerified}}
<div class="mb-6 rounded-md bg-green-50 p-4 ring-1 ring-green-200"> <div class="mb-6 rounded-md bg-green-50 p-4 ring-1 ring-green-200">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">