mirror of https://github.com/go-gitea/gitea.git
Support actions and reusable workflows from private repos (#32562)
Resolve https://gitea.com/gitea/act_runner/issues/102 This PR allows administrators of a private repository to specify some collaborative owners. The repositories of collaborative owners will be allowed to access this repository's actions and workflows. Settings for private repos:  --- This PR also moves "Enable Actions" setting to `Actions > General` page <img width="960" alt="image" src="https://github.com/user-attachments/assets/49337ec2-afb1-4a67-8516-5c9ef0ce05d4" /> <img width="960" alt="image" src="https://github.com/user-attachments/assets/f58ee6d5-17f9-4180-8760-a78e859f1c37" /> --------- Signed-off-by: Zettat123 <zettat123@gmail.com> Co-authored-by: ChristopherHX <christopher.homberger@web.de>pull/35748/head
parent
5454fdacd4
commit
c9beb0b01f
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tplRepoActionsGeneralSettings templates.TplName = "repo/settings/actions"
|
||||||
|
|
||||||
|
func ActionsGeneralSettings(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("actions.general")
|
||||||
|
ctx.Data["PageType"] = "general"
|
||||||
|
ctx.Data["PageIsActionsSettingsGeneral"] = true
|
||||||
|
|
||||||
|
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||||
|
if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
|
||||||
|
ctx.ServerError("GetUnit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if actionsUnit == nil { // no actions unit
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Repo.Repository.IsPrivate {
|
||||||
|
collaborativeOwnerIDs := actionsUnit.ActionsConfig().CollaborativeOwnerIDs
|
||||||
|
collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUsersByIDs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["CollaborativeOwners"] = collaborativeOwners
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActionsUnitPost(ctx *context.Context) {
|
||||||
|
redirectURL := ctx.Repo.RepoLink + "/settings/actions/general"
|
||||||
|
enableActionsUnit := ctx.FormBool("enable_actions")
|
||||||
|
repo := ctx.Repo.Repository
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if enableActionsUnit && !unit_model.TypeActions.UnitGlobalDisabled() {
|
||||||
|
err = repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{newRepoUnit(repo, unit_model.TypeActions, nil)}, nil)
|
||||||
|
} else if !unit_model.TypeActions.UnitGlobalDisabled() {
|
||||||
|
err = repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("UpdateRepositoryUnits", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||||
|
ctx.Redirect(redirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddCollaborativeOwner(ctx *context.Context) {
|
||||||
|
name := strings.ToLower(ctx.FormString("collaborative_owner"))
|
||||||
|
|
||||||
|
ownerID, err := user_model.GetUserOrOrgIDByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||||
|
ctx.JSONErrorNotFound()
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetUserOrOrgIDByName", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUnit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actionsCfg := actionsUnit.ActionsConfig()
|
||||||
|
actionsCfg.AddCollaborativeOwner(ownerID)
|
||||||
|
if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil {
|
||||||
|
ctx.ServerError("UpdateRepoUnit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSONOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCollaborativeOwner(ctx *context.Context) {
|
||||||
|
ownerID := ctx.FormInt64("id")
|
||||||
|
|
||||||
|
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUnit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actionsCfg := actionsUnit.ActionsConfig()
|
||||||
|
if !actionsCfg.IsCollaborativeOwner(ownerID) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("actions.general.collaborative_owner_not_exist"))
|
||||||
|
ctx.JSONErrorNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actionsCfg.RemoveCollaborativeOwner(ownerID)
|
||||||
|
if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil {
|
||||||
|
ctx.ServerError("UpdateRepoUnit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSONOK()
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<div class="repo-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "actions.general.enable_actions"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form" action="{{.Link}}/actions_unit" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
{{$isActionsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeActions}}
|
||||||
|
{{$isActionsGlobalDisabled := ctx.Consts.RepoUnitTypeActions.UnitGlobalDisabled}}
|
||||||
|
<div class="inline field">
|
||||||
|
<label>{{ctx.Locale.Tr "actions.actions"}}</label>
|
||||||
|
<div class="ui checkbox{{if $isActionsGlobalDisabled}} disabled{{end}}"{{if $isActionsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||||
|
<input class="enable-system" name="enable_actions" type="checkbox" {{if $isActionsGlobalDisabled}}disabled{{end}} {{if $isActionsEnabled}}checked{{end}}>
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.actions_desc"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if not $isActionsGlobalDisabled}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}}
|
||||||
|
{{if .Repository.IsPrivate}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "actions.general.collaborative_owners_management"}}
|
||||||
|
</h4>
|
||||||
|
{{if len .CollaborativeOwners}}
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div class="flex-list">
|
||||||
|
{{range .CollaborativeOwners}}
|
||||||
|
<div class="flex-item tw-items-center">
|
||||||
|
<div class="flex-item-leading">
|
||||||
|
<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
{{template "shared/user/name" .}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-trailing">
|
||||||
|
<button class="ui red tiny button inline link-action"
|
||||||
|
data-url="{{$.Link}}/collaborative_owner/delete?id={{.ID}}"
|
||||||
|
data-modal-confirm-header="{{ctx.Locale.Tr "actions.general.remove_collaborative_owner"}}"
|
||||||
|
data-modal-confirm-content="{{ctx.Locale.Tr "actions.general.remove_collaborative_owner_desc"}}"
|
||||||
|
>{{ctx.Locale.Tr "remove"}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="ui bottom attached segment">
|
||||||
|
<form class="ui form form-fetch-action" action="{{.Link}}/collaborative_owner/add" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div id="search-user-box" class="ui search input tw-align-middle" data-include-orgs="true">
|
||||||
|
<input class="prompt" name="collaborative_owner" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "actions.general.add_collaborative_owner"}}</button>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
{{ctx.Locale.Tr "actions.general.collaborative_owners_management_help"}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionsCollaborativeOwner(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
// user2 is the owner of "reusable_workflow" repo
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
user2Session := loginUser(t, user2.Name)
|
||||||
|
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
repo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
|
||||||
|
|
||||||
|
// a private repo(id=6) of user10 will try to clone "reusable_workflow" repo
|
||||||
|
user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
|
||||||
|
// task id is 55 and its repo_id=6
|
||||||
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 55, RepoID: 6})
|
||||||
|
taskToken := "674f727a81ed2f195bccab036cccf86a182199eb"
|
||||||
|
tokenHash := auth_model.HashToken(taskToken, task.TokenSalt)
|
||||||
|
assert.Equal(t, task.TokenHash, tokenHash)
|
||||||
|
|
||||||
|
dstPath := t.TempDir()
|
||||||
|
u.Path = fmt.Sprintf("%s/%s.git", repo.Owner.UserName, repo.Name)
|
||||||
|
u.User = url.UserPassword("gitea-actions", taskToken)
|
||||||
|
|
||||||
|
// the git clone will fail
|
||||||
|
doGitCloneFail(u)(t)
|
||||||
|
|
||||||
|
// add user10 to the list of collaborative owners
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", repo.Owner.UserName, repo.Name), map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, user2Session),
|
||||||
|
"collaborative_owner": user10.Name,
|
||||||
|
})
|
||||||
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// the git clone will be successful
|
||||||
|
doGitClone(dstPath, u)(t)
|
||||||
|
|
||||||
|
// remove user10 from the list of collaborative owners
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", repo.Owner.UserName, repo.Name, user10.ID), map[string]string{
|
||||||
|
"_csrf": GetUserCSRFToken(t, user2Session),
|
||||||
|
})
|
||||||
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// the git clone will fail
|
||||||
|
doGitCloneFail(u)(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue