mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-24 23:19:35 +02:00
feat: Merge the main branch
This commit is contained in:
@@ -104,7 +104,7 @@ import (
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -112,6 +112,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
@@ -162,13 +163,7 @@ func ArtifactsV4Routes(prefix string) *web.Router {
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
|
||||
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
||||
mac.Write([]byte(endpoint))
|
||||
mac.Write([]byte(expires))
|
||||
mac.Write([]byte(artifactName))
|
||||
_, _ = fmt.Fprint(mac, taskID)
|
||||
_, _ = fmt.Fprint(mac, artifactID)
|
||||
return mac.Sum(nil)
|
||||
return actions_module.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10))
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
gouuid "github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func NewRunnerServiceHandler() (string, http.Handler) {
|
||||
@@ -67,17 +69,19 @@ func (s *Service) Register(
|
||||
}
|
||||
|
||||
labels := req.Msg.Labels
|
||||
hasCancellingSupport, _ := runnerRequestHasCancellingCapability(req.Msg)
|
||||
|
||||
// create new runner
|
||||
name := util.EllipsisDisplayString(req.Msg.Name, 255)
|
||||
runner := &actions_model.ActionRunner{
|
||||
UUID: gouuid.New().String(),
|
||||
Name: name,
|
||||
OwnerID: runnerToken.OwnerID,
|
||||
RepoID: runnerToken.RepoID,
|
||||
Version: req.Msg.Version,
|
||||
AgentLabels: labels,
|
||||
Ephemeral: req.Msg.Ephemeral,
|
||||
UUID: gouuid.New().String(),
|
||||
Name: name,
|
||||
OwnerID: runnerToken.OwnerID,
|
||||
RepoID: runnerToken.RepoID,
|
||||
Version: req.Msg.Version,
|
||||
AgentLabels: labels,
|
||||
Ephemeral: req.Msg.Ephemeral,
|
||||
HasCancellingSupport: hasCancellingSupport,
|
||||
}
|
||||
runner.GenerateAndFillToken()
|
||||
|
||||
@@ -107,14 +111,53 @@ func (s *Service) Register(
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// runnerCapabilityCancelling is the wire string the runner advertises in its
|
||||
// capabilities list to indicate it understands the transitional cancelling
|
||||
// state and will run post-step cleanup before finalizing the task.
|
||||
const runnerCapabilityCancelling = "cancelling"
|
||||
|
||||
type capabilityGetter interface {
|
||||
GetCapabilities() []string
|
||||
}
|
||||
|
||||
type declareRequest interface {
|
||||
proto.Message
|
||||
GetVersion() string
|
||||
GetLabels() []string
|
||||
}
|
||||
|
||||
func runnerRequestHasCancellingCapability(req proto.Message) (bool, bool) {
|
||||
if req == nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
if typedReq, ok := any(req).(capabilityGetter); ok {
|
||||
return slices.Contains(typedReq.GetCapabilities(), runnerCapabilityCancelling), true
|
||||
}
|
||||
|
||||
return false, false
|
||||
}
|
||||
|
||||
func applyDeclareRequestToRunner(runner *actions_model.ActionRunner, req declareRequest) []string {
|
||||
runner.AgentLabels = req.GetLabels()
|
||||
runner.Version = req.GetVersion()
|
||||
|
||||
cols := []string{"agent_labels", "version"}
|
||||
hasCancellingSupport, capabilityStateKnown := runnerRequestHasCancellingCapability(req)
|
||||
if capabilityStateKnown && runner.HasCancellingSupport != hasCancellingSupport {
|
||||
runner.HasCancellingSupport = hasCancellingSupport
|
||||
cols = append(cols, "has_cancelling_support")
|
||||
}
|
||||
|
||||
return cols
|
||||
}
|
||||
|
||||
func (s *Service) Declare(
|
||||
ctx context.Context,
|
||||
req *connect.Request[runnerv1.DeclareRequest],
|
||||
) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
runner := GetRunner(ctx)
|
||||
runner.AgentLabels = req.Msg.Labels
|
||||
runner.Version = req.Msg.Version
|
||||
if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version"); err != nil {
|
||||
if err := actions_model.UpdateRunner(ctx, runner, applyDeclareRequestToRunner(runner, req.Msg)...); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
||||
}
|
||||
|
||||
@@ -261,7 +304,16 @@ func (s *Service) UpdateLog(
|
||||
}
|
||||
ack := task.LogLength
|
||||
|
||||
if len(req.Msg.Rows) == 0 || req.Msg.Index > ack || int64(len(req.Msg.Rows))+req.Msg.Index <= ack {
|
||||
// Trim rows the runner already had acked.
|
||||
var rows []*runnerv1.LogRow
|
||||
if req.Msg.Index <= ack && int64(len(req.Msg.Rows))+req.Msg.Index > ack {
|
||||
rows = req.Msg.Rows[ack-req.Msg.Index:]
|
||||
}
|
||||
|
||||
// Bail unless we have new rows or a NoMore to finalize. Even with
|
||||
// NoMore, bail when the runner has outrun the server — archiving a
|
||||
// log with a gap is worse than asking it to retry.
|
||||
if len(rows) == 0 && (!req.Msg.NoMore || req.Msg.Index > ack) {
|
||||
res.Msg.AckIndex = ack
|
||||
return res, nil
|
||||
}
|
||||
@@ -270,7 +322,9 @@ func (s *Service) UpdateLog(
|
||||
return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
|
||||
}
|
||||
|
||||
rows := req.Msg.Rows[ack-req.Msg.Index:]
|
||||
// WriteLogs is called even with no rows: with offset==0 it bootstraps
|
||||
// an empty DBFS file so TransferLogs below has something to read when
|
||||
// the runner finalizes a task that produced no log output.
|
||||
ns, err := actions.WriteLogs(ctx, task.LogFilename, task.LogSize, rows)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "unable to append logs to dbfs file: %v", err)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type capabilityRegisterRequest struct {
|
||||
*runnerv1.RegisterRequest
|
||||
capabilities []string
|
||||
}
|
||||
|
||||
func (r *capabilityRegisterRequest) GetCapabilities() []string {
|
||||
return r.capabilities
|
||||
}
|
||||
|
||||
type capabilityDeclareRequest struct {
|
||||
*runnerv1.DeclareRequest
|
||||
capabilities []string
|
||||
}
|
||||
|
||||
func (r *capabilityDeclareRequest) GetCapabilities() []string {
|
||||
return r.capabilities
|
||||
}
|
||||
|
||||
func TestRunnerRequestHasCancellingCapabilityTypedAccessor(t *testing.T) {
|
||||
registerReq := &capabilityRegisterRequest{
|
||||
RegisterRequest: &runnerv1.RegisterRequest{},
|
||||
capabilities: []string{runnerCapabilityCancelling, "other"},
|
||||
}
|
||||
hasCapability, known := runnerRequestHasCancellingCapability(registerReq)
|
||||
assert.True(t, hasCapability)
|
||||
assert.True(t, known)
|
||||
|
||||
declareReq := &capabilityDeclareRequest{
|
||||
DeclareRequest: &runnerv1.DeclareRequest{},
|
||||
capabilities: nil,
|
||||
}
|
||||
hasCapability, known = runnerRequestHasCancellingCapability(declareReq)
|
||||
assert.False(t, hasCapability)
|
||||
assert.True(t, known)
|
||||
|
||||
hasCapability, known = runnerRequestHasCancellingCapability(nil)
|
||||
assert.False(t, hasCapability)
|
||||
assert.False(t, known)
|
||||
}
|
||||
|
||||
func TestApplyDeclareRequestToRunnerPreservesUnknownCapabilityState(t *testing.T) {
|
||||
runner := &actions_model.ActionRunner{
|
||||
HasCancellingSupport: true,
|
||||
}
|
||||
req := &runnerv1.DeclareRequest{
|
||||
Version: "1.2.3",
|
||||
Labels: []string{"linux"},
|
||||
}
|
||||
|
||||
cols := applyDeclareRequestToRunner(runner, req)
|
||||
assert.Equal(t, []string{"agent_labels", "version"}, cols)
|
||||
assert.True(t, runner.HasCancellingSupport)
|
||||
assert.Equal(t, "1.2.3", runner.Version)
|
||||
assert.Equal(t, []string{"linux"}, runner.AgentLabels)
|
||||
}
|
||||
|
||||
func TestApplyDeclareRequestToRunnerUpdatesTypedCapabilityState(t *testing.T) {
|
||||
runner := &actions_model.ActionRunner{
|
||||
HasCancellingSupport: true,
|
||||
}
|
||||
req := &capabilityDeclareRequest{
|
||||
DeclareRequest: &runnerv1.DeclareRequest{
|
||||
Version: "1.2.3",
|
||||
Labels: []string{"linux"},
|
||||
},
|
||||
capabilities: []string{},
|
||||
}
|
||||
|
||||
cols := applyDeclareRequestToRunner(runner, req)
|
||||
assert.Equal(t, []string{"agent_labels", "version", "has_cancelling_support"}, cols)
|
||||
assert.False(t, runner.HasCancellingSupport)
|
||||
}
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ServiceIndexResponse contains registry endpoints
|
||||
@@ -91,7 +94,7 @@ type Source struct {
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
||||
func createPackageMetadataResponse(ctx *context.Context, registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
||||
versions := make([]*PackageVersionMetadata, 0, len(pds))
|
||||
|
||||
for _, pd := range pds {
|
||||
@@ -116,10 +119,15 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
||||
},
|
||||
}
|
||||
if pd.Repository != nil {
|
||||
pkg.Source = Source{
|
||||
URL: pd.Repository.HTMLURL(),
|
||||
Type: "git",
|
||||
Reference: pd.Version.Version,
|
||||
permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
log.Error("GetDoerRepoPermission[%d]: %v", pd.Repository.ID, err)
|
||||
} else if permission.HasAnyUnitAccessOrPublicAccess() {
|
||||
pkg.Source = Source{
|
||||
URL: pd.Repository.HTMLURL(),
|
||||
Type: "git",
|
||||
Reference: pd.Version.Version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ func PackageMetadata(ctx *context.Context) {
|
||||
}
|
||||
|
||||
resp := createPackageMetadataResponse(
|
||||
ctx,
|
||||
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
|
||||
pds,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,14 @@ func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// - name: sort
|
||||
// in: query
|
||||
// description: sort jobs by attribute. Supported values are "id". Default is "id"
|
||||
// type: string
|
||||
// - name: order
|
||||
// in: query
|
||||
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
|
||||
// type: string
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
@@ -36,6 +44,8 @@ func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.ListJobs(ctx, 0, 0, 0, nil)
|
||||
}
|
||||
|
||||
@@ -464,24 +464,9 @@ func SearchUsers(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
orderBy := db.SearchOrderByAlphabetically
|
||||
sortMode := ctx.FormString("sort")
|
||||
if len(sortMode) > 0 {
|
||||
sortOrder := ctx.FormString("order")
|
||||
if len(sortOrder) == 0 {
|
||||
sortOrder = "asc"
|
||||
}
|
||||
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
|
||||
if order, ok := searchModeMap[sortMode]; ok {
|
||||
orderBy = order
|
||||
} else {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
|
||||
return
|
||||
}
|
||||
orderBy, ok := utils.ResolveSortOrder(ctx, user_model.AdminUserOrderByMap, db.SearchOrderByAlphabetically)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var visible []api.VisibleType
|
||||
|
||||
+84
-61
@@ -212,6 +212,11 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.TokenCanAccessRepo(repo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,51 +254,66 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// public Only permission check
|
||||
switch {
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
|
||||
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
|
||||
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
||||
return
|
||||
for _, category := range requiredScopeCategories {
|
||||
switch category {
|
||||
case auth_model.AccessTokenScopeCategoryRepository:
|
||||
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryIssue:
|
||||
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryOrganization:
|
||||
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
|
||||
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
|
||||
if orgPrivate || userOrgPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryUser:
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryActivityPub:
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryNotification:
|
||||
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryPackage:
|
||||
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rejectPublicOnly() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if !ctx.PublicOnly {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
|
||||
}
|
||||
}
|
||||
|
||||
func contextAuthenticatedUser() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
ctx.ContextUser = ctx.Doer
|
||||
}
|
||||
}
|
||||
|
||||
// if a token is being used for auth, we check that it contains the required scope
|
||||
// if a token is not being used, reqToken will enforce other sign in methods
|
||||
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
|
||||
@@ -957,6 +977,8 @@ func Routes() *web.Router {
|
||||
})
|
||||
|
||||
// Notifications (requires 'notifications' scope)
|
||||
// The notifications API is not available for public-only tokens because a user's notifications mix
|
||||
// public and private repository events in the same mailbox.
|
||||
m.Group("/notifications", func() {
|
||||
m.Combo("").
|
||||
Get(reqToken(), notify.ListNotifications).
|
||||
@@ -965,7 +987,7 @@ func Routes() *web.Router {
|
||||
m.Combo("/threads/{id}").
|
||||
Get(reqToken(), notify.GetThread).
|
||||
Patch(reqToken(), notify.ReadThread)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
|
||||
|
||||
// Users (requires user scope)
|
||||
m.Group("/users", func() {
|
||||
@@ -1013,8 +1035,9 @@ func Routes() *web.Router {
|
||||
m.Group("/settings", func() {
|
||||
m.Get("", user.GetUserSettings)
|
||||
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
||||
}, reqToken())
|
||||
m.Combo("/emails").
|
||||
}, rejectPublicOnly())
|
||||
// Email addresses are always private account data.
|
||||
m.Combo("/emails", rejectPublicOnly()).
|
||||
Get(user.ListEmails).
|
||||
Post(bind(api.CreateEmailOption{}), user.AddEmail).
|
||||
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
|
||||
@@ -1046,7 +1069,7 @@ func Routes() *web.Router {
|
||||
|
||||
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
|
||||
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
|
||||
})
|
||||
}, rejectPublicOnly())
|
||||
|
||||
m.Get("/followers", user.ListMyFollowers)
|
||||
m.Group("/following", func() {
|
||||
@@ -1064,7 +1087,7 @@ func Routes() *web.Router {
|
||||
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
|
||||
m.Combo("/{id}").Get(user.GetPublicKey).
|
||||
Delete(user.DeletePublicKey)
|
||||
})
|
||||
}, rejectPublicOnly())
|
||||
|
||||
// (admin:application scope)
|
||||
m.Group("/applications", func() {
|
||||
@@ -1075,7 +1098,7 @@ func Routes() *web.Router {
|
||||
Delete(user.DeleteOauth2Application).
|
||||
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
|
||||
Get(user.GetOauth2Application)
|
||||
})
|
||||
}, rejectPublicOnly())
|
||||
|
||||
// (admin:gpg_key scope)
|
||||
m.Group("/gpg_keys", func() {
|
||||
@@ -1083,13 +1106,13 @@ func Routes() *web.Router {
|
||||
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
|
||||
m.Combo("/{id}").Get(user.GetGPGKey).
|
||||
Delete(user.DeleteGPGKey)
|
||||
})
|
||||
m.Get("/gpg_key_token", user.GetVerificationToken)
|
||||
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
|
||||
}, rejectPublicOnly())
|
||||
m.Get("/gpg_key_token", rejectPublicOnly(), user.GetVerificationToken)
|
||||
m.Post("/gpg_key_verify", rejectPublicOnly(), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
|
||||
|
||||
// (repo scope)
|
||||
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
|
||||
Post(bind(api.CreateRepoOption{}), repo.Create)
|
||||
Post(rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.Create)
|
||||
|
||||
// (repo scope)
|
||||
m.Group("/starred", func() {
|
||||
@@ -1100,22 +1123,22 @@ func Routes() *web.Router {
|
||||
m.Delete("", user.Unstar)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||
m.Get("/times", repo.ListMyTrackedTimes)
|
||||
m.Get("/stopwatches", repo.GetStopwatches)
|
||||
m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes)
|
||||
m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches)
|
||||
m.Get("/subscriptions", user.GetMyWatchedRepos)
|
||||
m.Get("/teams", org.ListUserTeams)
|
||||
m.Get("/teams", rejectPublicOnly(), org.ListUserTeams)
|
||||
m.Group("/hooks", func() {
|
||||
m.Combo("").Get(user.ListHooks).
|
||||
Post(bind(api.CreateHookOption{}), user.CreateHook)
|
||||
m.Combo("/{id}").Get(user.GetHook).
|
||||
Patch(bind(api.EditHookOption{}), user.EditHook).
|
||||
Delete(user.DeleteHook)
|
||||
}, reqWebhooksEnabled())
|
||||
}, reqWebhooksEnabled(), rejectPublicOnly())
|
||||
|
||||
m.Group("/avatar", func() {
|
||||
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
|
||||
m.Delete("", user.DeleteAvatar)
|
||||
})
|
||||
}, rejectPublicOnly())
|
||||
|
||||
m.Group("/blocks", func() {
|
||||
m.Get("", user.ListBlocks)
|
||||
@@ -1124,8 +1147,8 @@ func Routes() *web.Router {
|
||||
m.Put("", user.BlockUser)
|
||||
m.Delete("", user.UnblockUser)
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
}, rejectPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
|
||||
|
||||
// Repositories (requires repo scope, org scope)
|
||||
m.Post("/org/{org}/repos",
|
||||
@@ -1430,9 +1453,9 @@ func Routes() *web.Router {
|
||||
Delete(reqToken(), repo.DeleteTopic)
|
||||
}, reqAdmin())
|
||||
}, reqAnyRepoReader())
|
||||
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
|
||||
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
||||
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||
m.Get("/issue_config", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueConfig)
|
||||
m.Get("/issue_config/validate", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
||||
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
||||
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
|
||||
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
|
||||
@@ -1601,7 +1624,7 @@ func Routes() *web.Router {
|
||||
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
|
||||
|
||||
// Organizations
|
||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
|
||||
m.Group("/users/{username}/orgs", func() {
|
||||
m.Get("", reqToken(), org.ListUserOrgs)
|
||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||
|
||||
@@ -40,6 +40,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
UserID: u.ID,
|
||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -199,7 +200,7 @@ func GetAll(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/OrganizationList"
|
||||
|
||||
vMode := []api.VisibleType{api.VisibleTypePublic}
|
||||
if ctx.IsSigned && !ctx.PublicOnly {
|
||||
if ctx.IsSigned {
|
||||
vMode = append(vMode, api.VisibleTypeLimited)
|
||||
if ctx.Doer.IsAdmin {
|
||||
vMode = append(vMode, api.VisibleTypePrivate)
|
||||
@@ -208,13 +209,16 @@ func GetAll(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
searchOpts := user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
ListOptions: listOptions,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Visible: vMode,
|
||||
})
|
||||
}
|
||||
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -494,6 +498,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,6 @@ package repo
|
||||
import (
|
||||
go_context "context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -24,7 +23,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@@ -35,7 +33,7 @@ import (
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
secret_service "code.gitea.io/gitea/services/secrets"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
)
|
||||
|
||||
// ListActionsSecrets list an repo's actions secrets
|
||||
@@ -708,6 +706,14 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// - name: sort
|
||||
// in: query
|
||||
// description: sort jobs by attribute. Supported values are "id". Default is "id"
|
||||
// type: string
|
||||
// - name: order
|
||||
// in: query
|
||||
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
|
||||
// type: string
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
@@ -715,6 +721,8 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
@@ -1176,6 +1184,7 @@ func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.Ac
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
jobs.SortMatrixGroupsByName()
|
||||
return run, jobs
|
||||
}
|
||||
|
||||
@@ -1527,6 +1536,14 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// - name: sort
|
||||
// in: query
|
||||
// description: sort jobs by attribute. Supported values are "id". Default is "id"
|
||||
// type: string
|
||||
// - name: order
|
||||
// in: query
|
||||
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
|
||||
// type: string
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WorkflowJobsList"
|
||||
@@ -1534,6 +1551,8 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
|
||||
|
||||
@@ -1877,7 +1896,7 @@ func GetArtifact(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
if actions_service.IsArtifactV4(art) {
|
||||
convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -1926,7 +1945,7 @@ func DeleteArtifact(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
if actions_service.IsArtifactV4(art) {
|
||||
if err := actions_model.SetArtifactNeedDeleteByID(ctx, art.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -1939,11 +1958,7 @@ func DeleteArtifact(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
func buildSignature(endp string, expires, artifactID int64) []byte {
|
||||
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
||||
mac.Write([]byte(endp))
|
||||
fmt.Fprint(mac, expires)
|
||||
fmt.Fprint(mac, artifactID)
|
||||
return mac.Sum(nil)
|
||||
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
|
||||
}
|
||||
|
||||
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
|
||||
@@ -1999,10 +2014,10 @@ func DownloadArtifact(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
if actions_service.IsArtifactV4(art) {
|
||||
// @actions/toolkit asserts that downloaded artifacts of a different runid return 302
|
||||
// https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210
|
||||
if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) {
|
||||
if actions_service.DownloadArtifactV4ServeDirect(ctx.Base, art) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2054,8 +2069,8 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
|
||||
ctx.APIError(http.StatusNotFound, "Artifact has expired")
|
||||
return
|
||||
}
|
||||
if actions.IsArtifactV4(art) {
|
||||
err := actions.DownloadArtifactV4(ctx.Base, art)
|
||||
if actions_service.IsArtifactV4(art) {
|
||||
err := actions_service.DownloadArtifactV4(ctx.Base, art)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -711,7 +711,19 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
var bypassAllowlistUsers []int64
|
||||
if form.EnableBypassAllowlist {
|
||||
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
|
||||
if repo.Owner.IsOrganization() {
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
if err != nil {
|
||||
@@ -749,6 +761,17 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if form.EnableBypassAllowlist {
|
||||
bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protectBranch = &git_model.ProtectedBranch{
|
||||
@@ -762,6 +785,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
|
||||
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
|
||||
EnableMergeWhitelist: form.EnableMergeWhitelist,
|
||||
EnableBypassAllowlist: form.EnableBypassAllowlist,
|
||||
EnableStatusCheck: form.EnableStatusCheck,
|
||||
StatusCheckContexts: form.StatusCheckContexts,
|
||||
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
|
||||
@@ -786,6 +810,8 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
MergeTeamIDs: mergeWhitelistTeams,
|
||||
ApprovalsUserIDs: approvalsWhitelistUsers,
|
||||
ApprovalsTeamIDs: approvalsWhitelistTeams,
|
||||
BypassUserIDs: bypassAllowlistUsers,
|
||||
BypassTeamIDs: bypassAllowlistTeams,
|
||||
}); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -906,6 +932,10 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
|
||||
}
|
||||
|
||||
if form.EnableBypassAllowlist != nil {
|
||||
protectBranch.EnableBypassAllowlist = *form.EnableBypassAllowlist
|
||||
}
|
||||
|
||||
if form.EnableStatusCheck != nil {
|
||||
protectBranch.EnableStatusCheck = *form.EnableStatusCheck
|
||||
}
|
||||
@@ -958,7 +988,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride
|
||||
}
|
||||
|
||||
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64
|
||||
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers, bypassAllowlistUsers []int64
|
||||
if form.PushWhitelistUsernames != nil {
|
||||
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
|
||||
if err != nil {
|
||||
@@ -1011,8 +1041,21 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
|
||||
}
|
||||
if form.BypassAllowlistUsernames != nil {
|
||||
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
bypassAllowlistUsers = protectBranch.BypassAllowlistUserIDs
|
||||
}
|
||||
|
||||
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
|
||||
if repo.Owner.IsOrganization() {
|
||||
if form.PushWhitelistTeams != nil {
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
@@ -1066,6 +1109,23 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs
|
||||
}
|
||||
if form.BypassAllowlistTeams != nil {
|
||||
bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
bypassAllowlistTeams = protectBranch.BypassAllowlistTeamIDs
|
||||
}
|
||||
}
|
||||
if !protectBranch.EnableBypassAllowlist {
|
||||
bypassAllowlistUsers = nil
|
||||
bypassAllowlistTeams = nil
|
||||
}
|
||||
|
||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||
@@ -1077,6 +1137,8 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
MergeTeamIDs: mergeWhitelistTeams,
|
||||
ApprovalsUserIDs: approvalsWhitelistUsers,
|
||||
ApprovalsTeamIDs: approvalsWhitelistTeams,
|
||||
BypassUserIDs: bypassAllowlistUsers,
|
||||
BypassTeamIDs: bypassAllowlistTeams,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -47,9 +47,10 @@ func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPubl
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
opts.Private = !ctx.PublicOnly
|
||||
opts.Private = true
|
||||
opts.AllLimited = true
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
if ctx.FormString("owner") != "" {
|
||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
||||
if err != nil {
|
||||
|
||||
@@ -996,7 +996,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "Wrong commit ID") {
|
||||
ctx.JSON(http.StatusConflict, err)
|
||||
ctx.APIError(http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -1253,15 +1253,17 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
rebase := ctx.FormString("style") == "rebase"
|
||||
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
|
||||
// so existing API clients keep getting a merge update.
|
||||
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
|
||||
|
||||
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
|
||||
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pr, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
|
||||
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
+20
-20
@@ -134,9 +134,6 @@ func Search(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
|
||||
if ctx.PublicOnly {
|
||||
private = false
|
||||
}
|
||||
|
||||
opts := repo_model.SearchRepoOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
@@ -152,6 +149,7 @@ func Search(ctx *context.APIContext) {
|
||||
StarredByID: ctx.FormInt64("starredBy"),
|
||||
IncludeDescription: ctx.FormBool("includeDesc"),
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
if ctx.FormString("template") != "" {
|
||||
opts.Template = optional.Some(ctx.FormBool("template"))
|
||||
@@ -187,24 +185,11 @@ func Search(ctx *context.APIContext) {
|
||||
opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
|
||||
}
|
||||
|
||||
sortMode := ctx.FormString("sort")
|
||||
if len(sortMode) > 0 {
|
||||
sortOrder := ctx.FormString("order")
|
||||
if len(sortOrder) == 0 {
|
||||
sortOrder = "asc"
|
||||
}
|
||||
if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
|
||||
if orderBy, ok := searchModeMap[sortMode]; ok {
|
||||
opts.OrderBy = orderBy
|
||||
} else {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
|
||||
return
|
||||
}
|
||||
orderBy, ok := utils.ResolveSortOrder(ctx, repo_model.OrderByMap, "")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
opts.OrderBy = orderBy
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||
if err != nil {
|
||||
@@ -570,6 +555,10 @@ func GetByID(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if !ctx.TokenCanAccessRepo(repo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
@@ -898,10 +887,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
||||
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
|
||||
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
|
||||
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
|
||||
optional.AssignPtrValue(changed, &config.AllowMergeUpdate, opts.AllowMergeUpdate)
|
||||
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
|
||||
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
|
||||
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
|
||||
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
|
||||
optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle)
|
||||
// only validate update-style fields when the caller is actually changing one of them,
|
||||
// so unrelated PATCH calls don't reject historical configs.
|
||||
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
|
||||
if err := config.ValidateUpdateSettings(); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if *changed || mustInsertPullRequestUnit {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
@@ -1312,6 +1311,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,11 +36,16 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI
|
||||
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
|
||||
}
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
orderBy, ok := utils.ResolveSortOrder(ctx, actions_model.JobOrderByMap, actions_model.JobOrderByMap["asc"]["id"])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
opts := actions_model.FindRunJobOptions{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
RunID: runID,
|
||||
ListOptions: listOptions,
|
||||
OrderBy: orderBy,
|
||||
}
|
||||
if runID > 0 {
|
||||
opts.RunAttemptID = runAttemptID
|
||||
|
||||
@@ -429,6 +429,14 @@ func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// - name: sort
|
||||
// in: query
|
||||
// description: sort jobs by attribute. Supported values are "id". Default is "id"
|
||||
// type: string
|
||||
// - name: order
|
||||
// in: query
|
||||
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
|
||||
// type: string
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
@@ -438,6 +446,8 @@ func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.ListJobs(ctx, ctx.Doer.ID, 0, 0, nil)
|
||||
}
|
||||
|
||||
@@ -19,12 +19,15 @@ import (
|
||||
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
||||
opts := utils.GetListOptions(ctx)
|
||||
|
||||
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
searchOpts := repo_model.SearchRepoOptions{
|
||||
Actor: u,
|
||||
Private: private,
|
||||
ListOptions: opts,
|
||||
OrderBy: "id ASC",
|
||||
})
|
||||
}
|
||||
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -79,8 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
private := ctx.IsSigned
|
||||
listUserRepos(ctx, ctx.ContextUser, private)
|
||||
listUserRepos(ctx, ctx.ContextUser, ctx.IsSigned)
|
||||
}
|
||||
|
||||
// ListMyRepos - list the repositories you own or have access to.
|
||||
@@ -110,6 +112,7 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||
Private: ctx.IsSigned,
|
||||
IncludeDescription: true,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,11 +20,14 @@ import (
|
||||
// getStarredRepos returns the repos that the user with the specified userID has
|
||||
// starred
|
||||
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
|
||||
opts := &repo_model.StarredReposOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
StarrerID: user.ID,
|
||||
IncludePrivate: private,
|
||||
})
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
@@ -69,19 +68,16 @@ func Search(ctx *context.APIContext) {
|
||||
maxResults = 1
|
||||
users = []*user_model.User{user_model.NewActionsUser()}
|
||||
default:
|
||||
var visible []structs.VisibleType
|
||||
if ctx.PublicOnly {
|
||||
visible = []structs.VisibleType{structs.VisibleTypePublic}
|
||||
}
|
||||
users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
opts := user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
UID: uid,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
SearchByEmail: true,
|
||||
Visible: visible,
|
||||
ListOptions: listOptions,
|
||||
})
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
users, maxResults, err = user_model.SearchUsers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"ok": false,
|
||||
@@ -214,6 +210,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,11 +18,14 @@ import (
|
||||
|
||||
// getWatchedRepos returns the repos that the user with the specified userID is watching
|
||||
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
|
||||
opts := &repo_model.WatchedReposOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
WatcherID: user.ID,
|
||||
IncludePrivate: private,
|
||||
})
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ResolveSortOrder reads "sort" and "order" query params and returns the matching
|
||||
// SearchOrderBy from orderByMap. When "sort" is absent, returns defaultOrder.
|
||||
// On invalid input it writes a 422 response and returns ok=false; callers should
|
||||
// then return immediately.
|
||||
func ResolveSortOrder(ctx *context.APIContext, orderByMap map[string]map[string]db.SearchOrderBy, defaultOrder db.SearchOrderBy) (db.SearchOrderBy, bool) {
|
||||
sortMode := ctx.FormString("sort")
|
||||
if sortMode == "" {
|
||||
return defaultOrder, true
|
||||
}
|
||||
sortOrder := ctx.FormString("order")
|
||||
if sortOrder == "" {
|
||||
sortOrder = "asc"
|
||||
}
|
||||
orderMap, ok := orderByMap[sortOrder]
|
||||
if !ok {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: %q", sortOrder))
|
||||
return "", false
|
||||
}
|
||||
orderBy, ok := orderMap[sortMode]
|
||||
if !ok {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: %q", sortMode))
|
||||
return "", false
|
||||
}
|
||||
return orderBy, true
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResolveSortOrder(t *testing.T) {
|
||||
m := map[string]map[string]db.SearchOrderBy{
|
||||
"asc": {"id": "id ASC"},
|
||||
"desc": {"id": "id DESC"},
|
||||
}
|
||||
defaultOrder := db.SearchOrderBy("default")
|
||||
|
||||
cases := []struct {
|
||||
path string
|
||||
wantOK bool
|
||||
wantOrder db.SearchOrderBy
|
||||
wantStatus int
|
||||
}{
|
||||
{"GET /", true, defaultOrder, 0},
|
||||
{"GET /?sort=id", true, "id ASC", 0},
|
||||
{"GET /?sort=id&order=desc", true, "id DESC", 0},
|
||||
{"GET /?sort=bogus", false, "", http.StatusUnprocessableEntity},
|
||||
{"GET /?sort=id&order=bogus", false, "", http.StatusUnprocessableEntity},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockAPIContext(t, tc.path)
|
||||
got, ok := ResolveSortOrder(ctx, m, defaultOrder)
|
||||
assert.Equal(t, tc.wantOK, ok)
|
||||
assert.Equal(t, tc.wantOrder, got)
|
||||
assert.Equal(t, tc.wantStatus, ctx.Resp.WrittenStatus())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user