1
1
mirror of https://github.com/go-gitea/gitea synced 2025-12-07 13:28:25 +00:00

Merge branch 'main' into api-repo-actions

This commit is contained in:
Chester
2024-04-03 11:11:41 -04:00
committed by GitHub
2487 changed files with 63536 additions and 43295 deletions
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
package github.actions.results.api.v1;
message CreateArtifactRequest {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
string name = 3;
google.protobuf.Timestamp expires_at = 4;
int32 version = 5;
}
message CreateArtifactResponse {
bool ok = 1;
string signed_upload_url = 2;
}
message FinalizeArtifactRequest {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
string name = 3;
int64 size = 4;
google.protobuf.StringValue hash = 5;
}
message FinalizeArtifactResponse {
bool ok = 1;
int64 artifact_id = 2;
}
message ListArtifactsRequest {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
google.protobuf.StringValue name_filter = 3;
google.protobuf.Int64Value id_filter = 4;
}
message ListArtifactsResponse {
repeated ListArtifactsResponse_MonolithArtifact artifacts = 1;
}
message ListArtifactsResponse_MonolithArtifact {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
int64 database_id = 3;
string name = 4;
int64 size = 5;
google.protobuf.Timestamp created_at = 6;
}
message GetSignedArtifactURLRequest {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
string name = 3;
}
message GetSignedArtifactURLResponse {
string signed_url = 1;
}
message DeleteArtifactRequest {
string workflow_run_backend_id = 1;
string workflow_job_run_backend_id = 2;
string name = 3;
}
message DeleteArtifactResponse {
bool ok = 1;
int64 artifact_id = 2;
}
+61 -18
View File
@@ -70,7 +70,7 @@ import (
"strings"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -78,6 +78,8 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
)
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
@@ -137,12 +139,33 @@ func ArtifactContexter() func(next http.Handler) http.Handler {
return
}
authToken := strings.TrimPrefix(authHeader, "Bearer ")
task, err := actions.GetRunningTaskByToken(req.Context(), authToken)
if err != nil {
log.Error("Error runner api getting task: %v", err)
ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
return
// New act_runner uses jwt to authenticate
tID, err := actions_service.ParseAuthorizationToken(req)
var task *actions.ActionTask
if err == nil {
task, err = actions.GetTaskByID(req.Context(), tID)
if err != nil {
log.Error("Error runner api getting task by ID: %v", err)
ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
return
}
if task.Status != actions.StatusRunning {
log.Error("Error runner api getting task: task is not running")
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
return
}
} else {
// Old act_runner uses GITEA_TOKEN to authenticate
authToken := strings.TrimPrefix(authHeader, "Bearer ")
task, err = actions.GetRunningTaskByToken(req.Context(), authToken)
if err != nil {
log.Error("Error runner api getting task: %v", err)
ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
return
}
}
if err := task.LoadJob(req.Context()); err != nil {
@@ -257,8 +280,11 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
return
}
// update artifact size if zero
if artifact.FileSize == 0 || artifact.FileCompressedSize == 0 {
// update artifact size if zero or not match, over write artifact size
if artifact.FileSize == 0 ||
artifact.FileCompressedSize == 0 ||
artifact.FileSize != fileRealTotalSize ||
artifact.FileCompressedSize != chunksTotalSize {
artifact.FileSize = fileRealTotalSize
artifact.FileCompressedSize = chunksTotalSize
artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding")
@@ -267,6 +293,8 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
ctx.Error(http.StatusInternalServerError, "Error update artifact")
return
}
log.Debug("[artifact] update artifact size, artifact_id: %d, size: %d, compressed size: %d",
artifact.ID, artifact.FileSize, artifact.FileCompressedSize)
}
ctx.JSON(http.StatusOK, map[string]string{
@@ -314,7 +342,7 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
return
}
artifacts, err := actions.ListArtifactsByRunID(ctx, runID)
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
if err != nil {
log.Error("Error getting artifacts: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
@@ -376,7 +404,10 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
return
}
artifacts, err := actions.ListArtifactsByRunIDAndArtifactName(ctx, runID, itemPath)
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
RunID: runID,
ArtifactName: itemPath,
})
if err != nil {
log.Error("Error getting artifacts: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
@@ -396,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
var items []downloadArtifactResponseItem
for _, artifact := range artifacts {
downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
var downloadURL string
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
log.Error("Error getting serve direct url: %v", err)
}
if u != nil {
downloadURL = u.String()
}
}
if downloadURL == "" {
downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
}
item := downloadArtifactResponseItem{
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),
ItemType: "file",
@@ -419,15 +462,15 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
}
artifactID := ctx.ParamsInt64("artifact_id")
artifact, err := actions.GetArtifactByID(ctx, artifactID)
if errors.Is(err, util.ErrNotExist) {
log.Error("Error getting artifact: %v", err)
ctx.Error(http.StatusNotFound, err.Error())
return
} else if err != nil {
artifact, exist, err := db.GetByID[actions.ActionArtifact](ctx, artifactID)
if err != nil {
log.Error("Error getting artifact: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
return
} else if !exist {
log.Error("artifact with ID %d does not exist", artifactID)
ctx.Error(http.StatusNotFound, fmt.Sprintf("artifact with ID %d does not exist", artifactID))
return
}
if artifact.RunID != runID {
log.Error("Error dismatch runID and artifactID, task: %v, artifact: %v", runID, artifactID)
+99 -34
View File
@@ -5,18 +5,70 @@ package actions
import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"path/filepath"
"sort"
"strings"
"time"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
)
func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
contentSize, runID, start, end, length int64, checkMd5 bool,
) (int64, error) {
// build chunk store path
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
var r io.Reader = ctx.Req.Body
var hasher hash.Hash
if checkMd5 {
// use io.TeeReader to avoid reading all body to md5 sum.
// it writes data to hasher after reading end
// if hash is not matched, delete the read-end result
hasher = md5.New()
r = io.TeeReader(r, hasher)
}
// save chunk to storage
writtenSize, err := st.Save(storagePath, r, -1)
if err != nil {
return -1, fmt.Errorf("save chunk to storage error: %v", err)
}
var checkErr error
if checkMd5 {
// check md5
reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
// if md5 not match, delete the chunk
if reqMd5String != chunkMd5String {
checkErr = fmt.Errorf("md5 not match")
}
}
if writtenSize != contentSize {
checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
}
if checkErr != nil {
if err := st.Delete(storagePath); err != nil {
log.Error("Error deleting chunk: %s, %v", storagePath, err)
}
return -1, checkErr
}
log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
storagePath, contentSize, artifact.ID, start, end)
// return chunk total size
return length, nil
}
func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
contentSize, runID int64,
@@ -25,38 +77,22 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
contentRange := ctx.Req.Header.Get("Content-Range")
start, end, length := int64(0), int64(0), int64(0)
if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil {
log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
return -1, fmt.Errorf("parse content range error: %v", err)
}
// build chunk store path
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d.chunk", runID, artifact.ID, start, end)
// use io.TeeReader to avoid reading all body to md5 sum.
// it writes data to hasher after reading end
// if hash is not matched, delete the read-end result
hasher := md5.New()
r := io.TeeReader(ctx.Req.Body, hasher)
// save chunk to storage
writtenSize, err := st.Save(storagePath, r, -1)
if err != nil {
return -1, fmt.Errorf("save chunk to storage error: %v", err)
}
// check md5
reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
// if md5 not match, delete the chunk
if reqMd5String != chunkMd5String || writtenSize != contentSize {
if err := st.Delete(storagePath); err != nil {
log.Error("Error deleting chunk: %s, %v", storagePath, err)
}
return -1, fmt.Errorf("md5 not match")
}
log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
storagePath, contentSize, artifact.ID, start, end)
// return chunk total size
return length, nil
return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
}
func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
start, contentSize, runID int64,
) (int64, error) {
end := start + contentSize - 1
return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
}
type chunkFileItem struct {
RunID int64
ArtifactID int64
Start int64
End int64
@@ -66,9 +102,12 @@ type chunkFileItem struct {
func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chunkFileItem, error) {
storageDir := fmt.Sprintf("tmp%d", runID)
var chunks []*chunkFileItem
if err := st.IterateObjects(storageDir, func(path string, obj storage.Object) error {
item := chunkFileItem{Path: path}
if _, err := fmt.Sscanf(path, filepath.Join(storageDir, "%d-%d-%d.chunk"), &item.ArtifactID, &item.Start, &item.End); err != nil {
if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error {
baseName := filepath.Base(fpath)
// when read chunks from storage, it only contains storage dir and basename,
// no matter the subdirectory setting in storage config
item := chunkFileItem{Path: storageDir + "/" + baseName}
if _, err := fmt.Sscanf(baseName, "%d-%d-%d-%d.chunk", &item.RunID, &item.ArtifactID, &item.Start, &item.End); err != nil {
return fmt.Errorf("parse content range error: %v", err)
}
chunks = append(chunks, &item)
@@ -86,7 +125,10 @@ func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chun
func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error {
// read all db artifacts by name
artifacts, err := actions.ListArtifactsByRunIDAndName(ctx, runID, artifactName)
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
RunID: runID,
ArtifactName: artifactName,
})
if err != nil {
return err
}
@@ -102,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
log.Debug("artifact %d chunks not found", art.ID)
continue
}
if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil {
if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
return err
}
}
return nil
}
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error {
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].Start < chunks[j].Start
})
@@ -148,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
readers = append(readers, readCloser)
}
mergedReader := io.MultiReader(readers...)
shaPrefix := "sha256:"
var hash hash.Hash
if strings.HasPrefix(checksum, shaPrefix) {
hash = sha256.New()
}
if hash != nil {
mergedReader = io.TeeReader(mergedReader, hash)
}
// if chunk is gzip, use gz as extension
// download-artifact action will use content-encoding header to decide if it should decompress the file
@@ -176,8 +226,23 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
}
}()
if hash != nil {
rawChecksum := hash.Sum(nil)
actualChecksum := hex.EncodeToString(rawChecksum)
if !strings.HasSuffix(checksum, actualChecksum) {
return fmt.Errorf("update artifact error checksum is invalid")
}
}
// save storage path to artifact
log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath)
log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
// if artifact is already uploaded, delete the old file
if artifact.StoragePath != "" {
if err := st.Delete(artifact.StoragePath); err != nil {
log.Warn("Error deleting old artifact: %s, %v", artifact.StoragePath, err)
}
}
artifact.StoragePath = storagePath
artifact.Status = int64(actions.ArtifactStatusUploadConfirmed)
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+13 -1
View File
@@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true
}
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
task := ctx.ActionTask
runID, err := strconv.ParseInt(rawRunID, 10, 64)
if err != nil || task.Job.RunID != runID {
log.Error("Error runID not match")
ctx.Error(http.StatusBadRequest, "run-id does not match")
return nil, 0, false
}
return task, runID, true
}
func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
paramHash := ctx.Params("artifact_hash")
// use artifact name to create upload url
@@ -58,7 +69,8 @@ func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
// itemPath is generated from upload-artifact action
// it's formatted as {artifact_name}/{artfict_path_in_runner}
itemPath := util.PathJoinRel(ctx.Req.URL.Query().Get("itemPath"))
// act_runner in host mode on Windows, itemPath is joined by Windows slash '\'
itemPath := util.PathJoinRelX(ctx.Req.URL.Query().Get("itemPath"))
artifactName := strings.Split(itemPath, "/")[0]
artifactPath := strings.TrimPrefix(itemPath, artifactName+"/")
if !validateArtifactHash(ctx, artifactName) {
+512
View File
@@ -0,0 +1,512 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
// GitHub Actions Artifacts V4 API Simple Description
//
// 1. Upload artifact
// 1.1. CreateArtifact
// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact
// Request:
// {
// "workflow_run_backend_id": "21",
// "workflow_job_run_backend_id": "49",
// "name": "test",
// "version": 4
// }
// Response:
// {
// "ok": true,
// "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75"
// }
// 1.2. Upload Zip Content to Blobstorage (unauthenticated request)
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
// 1.5. FinalizeArtifact
// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
// Request
// {
// "workflow_run_backend_id": "21",
// "workflow_job_run_backend_id": "49",
// "name": "test",
// "size": "2097",
// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4"
// }
// Response
// {
// "ok": true,
// "artifactId": "4"
// }
// 2. Download artifact
// 2.1. ListArtifacts and optionally filter by artifact exact name or id
// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts
// Request
// {
// "workflow_run_backend_id": "21",
// "workflow_job_run_backend_id": "49",
// "name_filter": "test"
// }
// Response
// {
// "artifacts": [
// {
// "workflowRunBackendId": "21",
// "workflowJobRunBackendId": "49",
// "databaseId": "4",
// "name": "test",
// "size": "2093",
// "createdAt": "2024-01-23T00:13:28Z"
// }
// ]
// }
// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact
// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL
// Request
// {
// "workflow_run_backend_id": "21",
// "workflow_job_run_backend_id": "49",
// "name": "test"
// }
// Response
// {
// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76"
// }
// 2.3. Download Zip from Blobstorage (unauthenticated request)
// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"google.golang.org/protobuf/encoding/protojson"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService"
ArtifactV4ContentEncoding = "application/zip"
)
type artifactV4Routes struct {
prefix string
fs storage.ObjectStorage
}
func ArtifactV4Contexter() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := context.NewBaseContext(resp, req)
defer baseCleanUp()
ctx := &ArtifactContext{Base: base}
ctx.AppendContextValue(artifactContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
}
func ArtifactsV4Routes(prefix string) *web.Route {
m := web.NewRoute()
r := artifactV4Routes{
prefix: prefix,
fs: storage.ActionsArtifacts,
}
m.Group("", func() {
m.Post("CreateArtifact", r.createArtifact)
m.Post("FinalizeArtifact", r.finalizeArtifact)
m.Post("ListArtifacts", r.listArtifacts)
m.Post("GetSignedArtifactURL", r.getSignedArtifactURL)
m.Post("DeleteArtifact", r.deleteArtifact)
}, ArtifactContexter())
m.Group("", func() {
m.Put("UploadArtifact", r.uploadArtifact)
m.Get("DownloadArtifact", r.downloadArtifact)
}, ArtifactV4Contexter())
return m
}
func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endp))
mac.Write([]byte(expires))
mac.Write([]byte(artifactName))
mac.Write([]byte(fmt.Sprint(taskID)))
return mac.Sum(nil)
}
func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
return uploadURL
}
func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) {
rawTaskID := ctx.Req.URL.Query().Get("taskID")
sig := ctx.Req.URL.Query().Get("sig")
expires := ctx.Req.URL.Query().Get("expires")
artifactName := ctx.Req.URL.Query().Get("artifactName")
dsig, _ := base64.URLEncoding.DecodeString(sig)
taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
expecedsig := r.buildSignature(endp, expires, artifactName, taskID)
if !hmac.Equal(dsig, expecedsig) {
log.Error("Error unauthorized")
ctx.Error(http.StatusUnauthorized, "Error unauthorized")
return nil, "", false
}
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
if err != nil || t.Before(time.Now()) {
log.Error("Error link expired")
ctx.Error(http.StatusUnauthorized, "Error link expired")
return nil, "", false
}
task, err := actions.GetTaskByID(ctx, taskID)
if err != nil {
log.Error("Error runner api getting task by ID: %v", err)
ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
return nil, "", false
}
if task.Status != actions.StatusRunning {
log.Error("Error runner api getting task: task is not running")
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
return nil, "", false
}
if err := task.LoadJob(ctx); err != nil {
log.Error("Error runner api getting job: %v", err)
ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
return nil, "", false
}
return task, artifactName, true
}
func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) {
var art actions.ActionArtifact
has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art)
if err != nil {
return nil, err
} else if !has {
return nil, util.ErrNotExist
}
return &art, nil
}
func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
log.Error("Error decode request body: %v", err)
ctx.Error(http.StatusInternalServerError, "Error decode request body")
return false
}
err = protojson.Unmarshal(body, req)
if err != nil {
log.Error("Error decode request body: %v", err)
ctx.Error(http.StatusInternalServerError, "Error decode request body")
return false
}
return true
}
func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
resp, err := protojson.Marshal(req)
if err != nil {
log.Error("Error encode response body: %v", err)
ctx.Error(http.StatusInternalServerError, "Error encode response body")
return
}
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(resp)
}
func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
var req CreateArtifactRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
return
}
_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
artifactName := req.Name
rententionDays := setting.Actions.ArtifactRetentionDays
if req.ExpiresAt != nil {
rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
}
// create or get artifact with name and path
artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays)
if err != nil {
log.Error("Error create or get artifact: %v", err)
ctx.Error(http.StatusInternalServerError, "Error create or get artifact")
return
}
artifact.ContentEncoding = ArtifactV4ContentEncoding
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
log.Error("Error UpdateArtifactByID: %v", err)
ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
return
}
respData := CreateArtifactResponse{
Ok: true,
SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID),
}
r.sendProtbufBody(ctx, &respData)
}
func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact")
if !ok {
return
}
comp := ctx.Req.URL.Query().Get("comp")
switch comp {
case "block", "appendBlock":
// get artifact by name
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.Error(http.StatusNotFound, "Error artifact not found")
return
}
if comp == "block" {
artifact.FileSize = 0
artifact.FileCompressedSize = 0
}
_, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID)
if err != nil {
log.Error("Error runner api getting task: task is not running")
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
return
}
artifact.FileCompressedSize += ctx.Req.ContentLength
artifact.FileSize += ctx.Req.ContentLength
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
log.Error("Error UpdateArtifactByID: %v", err)
ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
return
}
ctx.JSON(http.StatusCreated, "appended")
case "blocklist":
ctx.JSON(http.StatusCreated, "created")
}
}
func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
var req FinalizeArtifactRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
// get artifact by name
artifact, err := r.getArtifactByName(ctx, runID, req.Name)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.Error(http.StatusNotFound, "Error artifact not found")
return
}
chunkMap, err := listChunksByRunID(r.fs, runID)
if err != nil {
log.Error("Error merge chunks: %v", err)
ctx.Error(http.StatusInternalServerError, "Error merge chunks")
return
}
chunks, ok := chunkMap[artifact.ID]
if !ok {
log.Error("Error merge chunks")
ctx.Error(http.StatusInternalServerError, "Error merge chunks")
return
}
checksum := ""
if req.Hash != nil {
checksum = req.Hash.Value
}
if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil {
log.Error("Error merge chunks: %v", err)
ctx.Error(http.StatusInternalServerError, "Error merge chunks")
return
}
respData := FinalizeArtifactResponse{
Ok: true,
ArtifactId: artifact.ID,
}
r.sendProtbufBody(ctx, &respData)
}
func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
var req ListArtifactsRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
if err != nil {
log.Error("Error getting artifacts: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
if len(artifacts) == 0 {
log.Debug("[artifact] handleListArtifacts, no artifacts")
ctx.Error(http.StatusNotFound)
return
}
list := []*ListArtifactsResponse_MonolithArtifact{}
table := map[string]*ListArtifactsResponse_MonolithArtifact{}
for _, artifact := range artifacts {
if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding {
table[artifact.ArtifactName] = nil
continue
}
table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{
Name: artifact.ArtifactName,
CreatedAt: timestamppb.New(artifact.CreatedUnix.AsTime()),
DatabaseId: artifact.ID,
WorkflowRunBackendId: req.WorkflowRunBackendId,
WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
Size: artifact.FileSize,
}
}
for _, artifact := range table {
if artifact != nil {
list = append(list, artifact)
}
}
respData := ListArtifactsResponse{
Artifacts: list,
}
r.sendProtbufBody(ctx, &respData)
}
func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
var req GetSignedArtifactURLRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
artifactName := req.Name
// get artifact by name
artifact, err := r.getArtifactByName(ctx, runID, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.Error(http.StatusNotFound, "Error artifact not found")
return
}
respData := GetSignedArtifactURLResponse{}
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
if u != nil && err == nil {
respData.SignedUrl = u.String()
}
}
if respData.SignedUrl == "" {
respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID)
}
r.sendProtbufBody(ctx, &respData)
}
func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
if !ok {
return
}
// get artifact by name
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.Error(http.StatusNotFound, "Error artifact not found")
return
}
file, _ := r.fs.Open(artifact.StoragePath)
_, _ = io.Copy(ctx.Resp, file)
}
func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
var req DeleteArtifactRequest
if ok := r.parseProtbufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
// get artifact by name
artifact, err := r.getArtifactByName(ctx, runID, req.Name)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.Error(http.StatusNotFound, "Error artifact not found")
return
}
err = actions.SetArtifactNeedDelete(ctx, runID, req.Name)
if err != nil {
log.Error("Error deleting artifacts: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
respData := DeleteArtifactResponse{
Ok: true,
ArtifactId: artifact.ID,
}
r.sendProtbufBody(ctx, &respData)
}
+2 -4
View File
@@ -12,7 +12,7 @@ import (
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
)
func NewPingServiceHandler() (string, http.Handler) {
@@ -21,9 +21,7 @@ func NewPingServiceHandler() (string, http.Handler) {
var _ pingv1connect.PingServiceHandler = (*Service)(nil)
type Service struct {
pingv1connect.UnimplementedPingServiceHandler
}
type Service struct{}
func (s *Service) Ping(
ctx context.Context,
+1 -1
View File
@@ -11,7 +11,7 @@ import (
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
+2 -4
View File
@@ -16,7 +16,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
gouuid "github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -32,9 +32,7 @@ func NewRunnerServiceHandler() (string, http.Handler) {
var _ runnerv1connect.RunnerServiceClient = (*Service)(nil)
type Service struct {
runnerv1connect.UnimplementedRunnerServiceHandler
}
type Service struct{}
// Register for new runner.
func (s *Service) Register(
+20 -63
View File
@@ -8,13 +8,13 @@ import (
"fmt"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
secret_module "code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/actions"
@@ -31,14 +31,24 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
return nil, false, nil
}
secrets, err := secret_model.GetSecretsOfTask(ctx, t)
if err != nil {
return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
}
vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
if err != nil {
return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
}
actions.CreateCommitStatus(ctx, t.Job)
task := &runnerv1.Task{
Id: t.ID,
WorkflowPayload: t.Job.WorkflowPayload,
Context: generateTaskContext(t),
Secrets: getSecretsOfTask(ctx, t),
Vars: getVariablesOfTask(ctx, t),
Secrets: secrets,
Vars: vars,
}
if needs, err := findTaskNeeds(ctx, t); err != nil {
@@ -54,65 +64,6 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
return task, true, nil
}
func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
secrets := map[string]string{}
secrets["GITHUB_TOKEN"] = task.Token
secrets["GITEA_TOKEN"] = task.Token
if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
return secrets
}
ownerSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
if err != nil {
log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
// go on
}
repoSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID})
if err != nil {
log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
// go on
}
for _, secret := range append(ownerSecrets, repoSecrets...) {
if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil {
log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
// go on
} else {
secrets[secret.Name] = v
}
}
return secrets
}
func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
variables := map[string]string{}
// Org / User level
ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
if err != nil {
log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
}
// Repo level
repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
if err != nil {
log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
}
// Level precedence: Repo > Org / User
for _, v := range append(ownerVariables, repoVariables...) {
variables[v.Name] = v.Data
}
return variables
}
func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
event := map[string]any{}
_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
@@ -144,6 +95,11 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
refName := git.RefName(ref)
giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
if err != nil {
log.Error("actions.CreateAuthorizationToken failed: %v", err)
}
taskContext, err := structpb.NewStruct(map[string]any{
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
"action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2.
@@ -183,6 +139,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
// additional contexts
"gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(),
"gitea_runtime_token": giteaRuntimeToken,
})
if err != nil {
log.Error("structpb.NewStruct failed: %v", err)
@@ -200,7 +157,7 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str
}
needs := container.SetOf(task.Job.Needs...)
jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID})
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID})
if err != nil {
return nil, fmt.Errorf("FindRunJobs: %w", err)
}
+27 -8
View File
@@ -14,12 +14,12 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
alpine_service "code.gitea.io/gitea/services/packages/alpine"
)
@@ -72,7 +72,7 @@ func GetRepositoryFile(ctx *context.Context) {
ctx,
pv,
&packages_service.PackageFileInfo{
Filename: alpine_service.IndexFilename,
Filename: alpine_service.IndexArchiveFilename,
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
},
)
@@ -182,19 +182,38 @@ func UploadPackageFile(ctx *context.Context) {
}
func DownloadPackageFile(ctx *context.Context) {
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
branch := ctx.Params("branch")
repository := ctx.Params("repository")
architecture := ctx.Params("architecture")
opts := &packages_model.PackageFileSearchOptions{
OwnerID: ctx.Package.Owner.ID,
PackageType: packages_model.TypeAlpine,
Query: ctx.Params("filename"),
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
})
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
}
pfs, _, err := packages_model.SearchFiles(ctx, opts)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pfs) != 1 {
apiError(ctx, http.StatusNotFound, nil)
return
if len(pfs) == 0 {
// Try again with architecture 'noarch'
if architecture == alpine_module.NoArch {
apiError(ctx, http.StatusNotFound, nil)
return
}
opts.CompositeKey = fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch)
if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pfs) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
}
s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+75 -12
View File
@@ -10,7 +10,6 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@@ -36,7 +35,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context"
)
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
@@ -513,14 +512,75 @@ func CommonRoutes() *web.Route {
r.Get("/simple/{id}", pypi.PackageMetadata)
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/rpm", func() {
r.Get(".repo", rpm.GetRepositoryConfig)
r.Get("/repository.key", rpm.GetRepositoryKey)
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
r.Group("/package/{name}/{version}/{architecture}", func() {
r.Get("", rpm.DownloadPackageFile)
r.Delete("", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
r.Group("/repository.key", func() {
r.Head("", rpm.GetRepositoryKey)
r.Get("", rpm.GetRepositoryKey)
})
var (
repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`)
uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`)
filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`)
repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`)
)
r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
path := ctx.Params("*")
isHead := ctx.Req.Method == "HEAD"
isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET"
isPut := ctx.Req.Method == "PUT"
isDelete := ctx.Req.Method == "DELETE"
m := repoPattern.FindStringSubmatch(path)
if len(m) == 2 && isGetHead {
ctx.SetParams("group", strings.Trim(m[1], "/"))
rpm.GetRepositoryConfig(ctx)
return
}
m = repoFilePattern.FindStringSubmatch(path)
if len(m) == 3 && isGetHead {
ctx.SetParams("group", strings.Trim(m[1], "/"))
ctx.SetParams("filename", m[2])
if isHead {
rpm.CheckRepositoryFileExistence(ctx)
} else {
rpm.GetRepositoryFile(ctx)
}
return
}
m = uploadPattern.FindStringSubmatch(path)
if len(m) == 2 && isPut {
reqPackageAccess(perm.AccessModeWrite)(ctx)
if ctx.Written() {
return
}
ctx.SetParams("group", strings.Trim(m[1], "/"))
rpm.UploadPackageFile(ctx)
return
}
m = filePattern.FindStringSubmatch(path)
if len(m) == 6 && (isGetHead || isDelete) {
ctx.SetParams("group", strings.Trim(m[1], "/"))
ctx.SetParams("name", m[2])
ctx.SetParams("version", m[3])
ctx.SetParams("architecture", m[4])
if isGetHead {
rpm.DownloadPackageFile(ctx)
} else {
reqPackageAccess(perm.AccessModeWrite)(ctx)
if ctx.Written() {
return
}
rpm.DeletePackageFile(ctx)
}
return
}
ctx.Status(http.StatusNotFound)
})
r.Get("/repodata/{filename}", rpm.GetRepositoryFile)
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/rubygems", func() {
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
@@ -581,7 +641,7 @@ func CommonRoutes() *web.Route {
})
})
}, reqPackageAccess(perm.AccessModeRead))
}, context_service.UserAssignmentWeb(), context.PackageAssignment())
}, context.UserAssignmentWeb(), context.PackageAssignment())
return r
}
@@ -600,7 +660,10 @@ func ContainerRoutes() *web.Route {
})
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
r.Get("/token", container.Authenticate)
r.Group("/token", func() {
r.Get("", container.Authenticate)
r.Post("", container.AuthenticateNotImplemented)
})
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
r.Group("/{username}", func() {
r.Group("/{image}", func() {
@@ -748,7 +811,7 @@ func ContainerRoutes() *web.Route {
ctx.Status(http.StatusNotFound)
})
}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
}, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
return r
}
+5 -4
View File
@@ -12,14 +12,15 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
@@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeCargo,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: &paginator,
},
)
@@ -250,7 +251,7 @@ func UploadPackage(ctx *context.Context) {
return
}
if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
log.Error("Rollback creation of package version: %v", err)
}
@@ -301,7 +302,7 @@ func yankPackage(ctx *context.Context, yank bool) {
return
}
if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
+1 -2
View File
@@ -8,6 +8,7 @@ import (
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
@@ -26,8 +27,6 @@ import (
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth"
"github.com/minio/sha256-simd"
)
const (
+4 -3
View File
@@ -15,12 +15,13 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeChef,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeChef,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("start"),
ctx.FormInt("items"),
+3 -2
View File
@@ -14,12 +14,13 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
@@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeComposer,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: &paginator,
}
if ctx.FormTrim("type") != "" {
+1 -1
View File
@@ -15,13 +15,13 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
)
+1 -1
View File
@@ -9,9 +9,9 @@ import (
conan_model "code.gitea.io/gitea/models/packages/conan"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/services/context"
)
// SearchResult contains the found recipe names
+1 -1
View File
@@ -12,13 +12,13 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
conda_model "code.gitea.io/gitea/models/packages/conda"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
conda_module "code.gitea.io/gitea/modules/packages/conda"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/dsnet/compress/bzip2"
+26 -6
View File
@@ -17,7 +17,6 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
@@ -25,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
@@ -114,11 +114,15 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
})
}
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
func apiUnauthorizedError(ctx *context.Context) {
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized)
}
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil {
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized)
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
apiUnauthorizedError(ctx)
}
}
@@ -138,10 +142,15 @@ func DetermineSupport(ctx *context.Context) {
}
// Authenticate creates a token for the current user
// If the current user is anonymous, the ghost user is used
// If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
func Authenticate(ctx *context.Context) {
u := ctx.Doer
if u == nil {
if setting.Service.RequireSignInView {
apiUnauthorizedError(ctx)
return
}
u = user_model.NewGhostUser()
}
@@ -156,6 +165,17 @@ func Authenticate(ctx *context.Context) {
})
}
// https://distribution.github.io/distribution/spec/auth/oauth/
func AuthenticateNotImplemented(ctx *context.Context) {
// This optional endpoint can be used to authenticate a client.
// It must implement the specification described in:
// https://datatracker.ietf.org/doc/html/rfc6749
// https://distribution.github.io/distribution/spec/auth/oauth/
// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
ctx.Status(http.StatusNotFound)
}
// https://docs.docker.com/registry/spec/api/#listing-repositories
func GetRepositoryList(ctx *context.Context) {
n := ctx.FormInt("n")
+1 -1
View File
@@ -13,11 +13,11 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
cran_model "code.gitea.io/gitea/models/packages/cran"
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
cran_module "code.gitea.io/gitea/modules/packages/cran"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
+1 -1
View File
@@ -13,11 +13,11 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
debian_module "code.gitea.io/gitea/modules/packages/debian"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
debian_service "code.gitea.io/gitea/services/packages/debian"
+25 -6
View File
@@ -8,18 +8,19 @@ import (
"net/http"
"regexp"
"strings"
"unicode"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
var (
packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`)
filenameRegex = packageNameRegex
packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -54,20 +55,38 @@ func DownloadPackageFile(ctx *context.Context) {
helper.ServePackageFile(ctx, s, u, pf)
}
func isValidPackageName(packageName string) bool {
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
return false
}
return packageNameRegex.MatchString(packageName) && packageName != ".."
}
func isValidFileName(filename string) bool {
return filenameRegex.MatchString(filename) &&
strings.TrimSpace(filename) == filename &&
filename != "." && filename != ".."
}
// UploadPackage uploads the specific generic package.
// Duplicated packages get rejected.
func UploadPackage(ctx *context.Context) {
packageName := ctx.Params("packagename")
filename := ctx.Params("filename")
if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
apiError(ctx, http.StatusBadRequest, errors.New("Invalid package name or filename"))
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
if !isValidFileName(filename) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid filename"))
return
}
packageVersion := ctx.Params("packageversion")
if packageVersion != strings.TrimSpace(packageVersion) {
apiError(ctx, http.StatusBadRequest, errors.New("Invalid package version"))
apiError(ctx, http.StatusBadRequest, errors.New("invalid package version"))
return
}
@@ -0,0 +1,65 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package generic
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidatePackageName(t *testing.T) {
bad := []string{
"",
".",
"..",
"-",
"a?b",
"a b",
"a/b",
}
for _, name := range bad {
assert.False(t, isValidPackageName(name), "bad=%q", name)
}
good := []string{
"a",
"1",
"a-",
"a_b",
"c.d+",
}
for _, name := range good {
assert.True(t, isValidPackageName(name), "good=%q", name)
}
}
func TestValidateFileName(t *testing.T) {
bad := []string{
"",
".",
"..",
"a?b",
"a/b",
" a",
"a ",
}
for _, name := range bad {
assert.False(t, isValidFileName(name), "bad=%q", name)
}
good := []string{
"-",
"a",
"1",
"a-",
"a_b",
"a b",
"c.d+",
`-_+=:;.()[]{}~!@#$%^& aA1`,
}
for _, name := range good {
assert.True(t, isValidFileName(name), "good=%q", name)
}
}
+3 -2
View File
@@ -12,11 +12,12 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (
Value: name,
ExactMatch: true,
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
})
if err != nil {
+4 -3
View File
@@ -13,14 +13,15 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
helm_module "code.gitea.io/gitea/modules/packages/helm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"gopkg.in/yaml.v3"
@@ -42,7 +43,7 @@ func Index(ctx *context.Context) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeHelm,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) {
Value: ctx.Params("package"),
},
HasFileWithName: filename,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
+1 -1
View File
@@ -10,9 +10,9 @@ import (
"net/url"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
)
// LogAndProcessError logs an error and calls a custom callback with the processed error message.
+2 -3
View File
@@ -6,6 +6,7 @@ package maven
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"encoding/xml"
@@ -19,15 +20,13 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
maven_module "code.gitea.io/gitea/modules/packages/maven"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/minio/sha256-simd"
)
const (
+2 -1
View File
@@ -12,6 +12,7 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
)
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
@@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
Maintainers: []npm_module.User{}, // npm cli needs this field
Keywords: metadata.Keywords,
Links: &npm_module.PackageSearchPackageLinks{
Registry: pd.FullWebLink(),
Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
Homepage: metadata.ProjectURL,
},
},
+5 -4
View File
@@ -17,12 +17,13 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
@@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
Value: packageNameFromParams(ctx),
},
HasFileWithName: filename,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
Properties: map[string]string{
npm_module.TagProperty: tag,
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
return err
@@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) {
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNpm,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Name: packages_model.SearchValue{
ExactMatch: false,
Value: ctx.FormTrim("text"),
+7 -6
View File
@@ -17,13 +17,14 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
nuget_model "code.gitea.io/gitea/models/packages/nuget"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) {
Name: packages_model.SearchValue{
Value: getSearchTerm(ctx),
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: paginator,
})
if err != nil {
@@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) {
Name: packages_model.SearchValue{
Value: getSearchTerm(ctx),
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) {
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("skip"),
ctx.FormInt("take"),
@@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
ExactMatch: true,
Value: packageName,
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: paginator,
})
if err != nil {
@@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) {
ExactMatch: true,
Value: strings.Trim(ctx.FormTrim("id"), "'"),
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
+1 -1
View File
@@ -14,7 +14,6 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
@@ -22,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
+1 -1
View File
@@ -12,12 +12,12 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
pypi_module "code.gitea.io/gitea/modules/packages/pypi"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
+55 -13
View File
@@ -13,13 +13,13 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
rpm_service "code.gitea.io/gitea/services/packages/rpm"
@@ -33,11 +33,18 @@ func apiError(ctx *context.Context, status int, obj any) {
// https://dnf.readthedocs.io/en/latest/conf_ref.html
func GetRepositoryConfig(ctx *context.Context) {
group := ctx.Params("group")
var groupParts []string
if group != "" {
groupParts = strings.Split(group, "/")
}
url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name)
ctx.PlainText(http.StatusOK, `[gitea-`+ctx.Package.Owner.LowerName+`]
name=`+ctx.Package.Owner.Name+` - `+setting.AppName+`
baseurl=`+url+`
ctx.PlainText(http.StatusOK, `[gitea-`+strings.Join(append([]string{ctx.Package.Owner.LowerName}, groupParts...), "-")+`]
name=`+strings.Join(append([]string{ctx.Package.Owner.Name, setting.AppName}, groupParts...), " - ")+`
baseurl=`+strings.Join(append([]string{url}, groupParts...), "/")+`
enabled=1
gpgcheck=1
gpgkey=`+url+`/repository.key`)
@@ -57,6 +64,30 @@ func GetRepositoryKey(ctx *context.Context) {
})
}
func CheckRepositoryFileExistence(ctx *context.Context) {
pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, ctx.Params("filename"), ctx.Params("group"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Status(http.StatusNotFound)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.SetServeHeaders(&context.ServeHeaderOptions{
Filename: pf.Name,
LastModified: pf.CreatedUnix.AsLocalTime(),
})
ctx.Status(http.StatusOK)
}
// Gets a pre-generated repository metadata file
func GetRepositoryFile(ctx *context.Context) {
pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
@@ -69,7 +100,8 @@ func GetRepositoryFile(ctx *context.Context) {
ctx,
pv,
&packages_service.PackageFileInfo{
Filename: ctx.Params("filename"),
Filename: ctx.Params("filename"),
CompositeKey: ctx.Params("group"),
},
)
if err != nil {
@@ -121,7 +153,7 @@ func UploadPackageFile(ctx *context.Context) {
apiError(ctx, http.StatusInternalServerError, err)
return
}
group := ctx.Params("group")
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
ctx,
&packages_service.PackageCreationInfo{
@@ -136,13 +168,16 @@ func UploadPackageFile(ctx *context.Context) {
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture),
Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture),
CompositeKey: group,
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
Properties: map[string]string{
rpm_module.PropertyMetadata: string(fileMetadataRaw),
rpm_module.PropertyGroup: group,
rpm_module.PropertyArchitecture: pck.FileMetadata.Architecture,
rpm_module.PropertyMetadata: string(fileMetadataRaw),
},
},
)
@@ -158,7 +193,7 @@ func UploadPackageFile(ctx *context.Context) {
return
}
if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID); err != nil {
if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -179,7 +214,8 @@ func DownloadPackageFile(ctx *context.Context) {
Version: version,
},
&packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
CompositeKey: ctx.Params("group"),
},
)
if err != nil {
@@ -195,6 +231,7 @@ func DownloadPackageFile(ctx *context.Context) {
}
func DeletePackageFile(webctx *context.Context) {
group := webctx.Params("group")
name := webctx.Params("name")
version := webctx.Params("version")
architecture := webctx.Params("architecture")
@@ -202,7 +239,12 @@ func DeletePackageFile(webctx *context.Context) {
var pd *packages_model.PackageDescriptor
err := db.WithTx(webctx, func(ctx stdctx.Context) error {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, webctx.Package.Owner.ID, packages_model.TypeRpm, name, version)
pv, err := packages_model.GetVersionByNameAndVersion(ctx,
webctx.Package.Owner.ID,
packages_model.TypeRpm,
name,
version,
)
if err != nil {
return err
}
@@ -211,7 +253,7 @@ func DeletePackageFile(webctx *context.Context) {
ctx,
pv.ID,
fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture),
packages_model.EmptyFileKey,
group,
)
if err != nil {
return err
@@ -251,7 +293,7 @@ func DeletePackageFile(webctx *context.Context) {
notify_service.PackageDelete(webctx, webctx.Doer, pd)
}
if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID); err != nil {
if err := rpm_service.BuildSpecificRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil {
apiError(webctx, http.StatusInternalServerError, err)
return
}
+4 -3
View File
@@ -13,11 +13,12 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
HasFileWithName: filename,
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
return pvs, err
}
+4 -3
View File
@@ -13,14 +13,15 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
swift_module "code.gitea.io/gitea/modules/packages/swift"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
@@ -157,7 +158,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
}
type Resource struct {
Name string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Checksum string `json:"checksum"`
}
@@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) {
Properties: map[string]string{
swift_module.PropertyRepositoryURL: url,
},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
+1 -1
View File
@@ -12,11 +12,11 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
+1 -1
View File
@@ -9,9 +9,9 @@ import (
"strings"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
+1 -1
View File
@@ -13,9 +13,9 @@ import (
"net/url"
"code.gitea.io/gitea/modules/activitypub"
gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-fed/httpsig"
+1 -1
View File
@@ -8,9 +8,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
+1 -1
View File
@@ -6,11 +6,11 @@ package admin
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/cron"
)
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
api "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"
)
+3 -2
View File
@@ -8,12 +8,13 @@ import (
"net/http"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context"
"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"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
)
@@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/HookList"
sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
return
+1 -1
View File
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+1 -1
View File
@@ -4,10 +4,10 @@
package admin
import (
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/services/context"
)
// CreateRepo api for creating a repository
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register global runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /admin/runners/registration-token admin adminGetRunnerRegistrationToken
// ---
// summary: Get an global actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, 0)
}
+83 -115
View File
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -16,16 +15,16 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
@@ -93,26 +92,32 @@ func CreateUser(ctx *context.APIContext) {
if ctx.Written() {
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
if err != nil {
log.Error(err.Error())
if u.LoginType == auth.Plain {
if len(form.Password) < setting.MinPasswordLength {
err := errors.New("PasswordIsRequired")
ctx.Error(http.StatusBadRequest, "PasswordIsRequired", err)
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
return
}
if err := password.IsPwned(ctx, form.Password); err != nil {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
return
}
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
return
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: util.OptionalBoolTrue,
}
if form.Restricted != nil {
overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
IsActive: optional.Some(true),
IsRestricted: optional.FromPtr(form.Restricted),
}
if form.Visibility != "" {
@@ -128,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
u.UpdatedUnix = u.CreatedUnix
}
if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) ||
db.IsErrNameReserved(err) ||
@@ -142,6 +147,11 @@ func CreateUser(ctx *context.APIContext) {
}
return
}
if !user_model.IsEmailDomainAllowed(u.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
}
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
@@ -173,6 +183,8 @@ func EditUser(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/User"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
@@ -180,111 +192,69 @@ func EditUser(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditUserOption)
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
if ctx.Written() {
authOpts := &user_service.UpdateAuthOptions{
LoginSource: optional.FromNonDefault(form.SourceID),
LoginName: optional.Some(form.LoginName),
Password: optional.FromNonDefault(form.Password),
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
}
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err)
default:
ctx.Error(http.StatusInternalServerError, "UpdateAuth", err)
}
return
}
if len(form.Password) != 0 {
if len(form.Password) < setting.MinPasswordLength {
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
if err != nil {
log.Error(err.Error())
}
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
return
}
if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
return
}
if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
ctx.InternalServerError(err)
return
}
}
if form.MustChangePassword != nil {
ctx.ContextUser.MustChangePassword = *form.MustChangePassword
}
ctx.ContextUser.LoginName = form.LoginName
if form.FullName != nil {
ctx.ContextUser.FullName = *form.FullName
}
var emailChanged bool
if form.Email != nil {
email := strings.TrimSpace(*form.Email)
if len(email) == 0 {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string"))
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Error(http.StatusBadRequest, "EmailUsed", err)
default:
ctx.Error(http.StatusInternalServerError, "AddOrSetPrimaryEmailAddress", err)
}
return
}
if err := user_model.ValidateEmail(email); err != nil {
ctx.InternalServerError(err)
return
if !user_model.IsEmailDomainAllowed(*form.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
}
emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
ctx.ContextUser.Email = email
}
if form.Website != nil {
ctx.ContextUser.Website = *form.Website
}
if form.Location != nil {
ctx.ContextUser.Location = *form.Location
}
if form.Description != nil {
ctx.ContextUser.Description = *form.Description
}
if form.Active != nil {
ctx.ContextUser.IsActive = *form.Active
}
if len(form.Visibility) != 0 {
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
}
if form.Admin != nil {
ctx.ContextUser.IsAdmin = *form.Admin
}
if form.AllowGitHook != nil {
ctx.ContextUser.AllowGitHook = *form.AllowGitHook
}
if form.AllowImportLocal != nil {
ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
}
if form.MaxRepoCreation != nil {
ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
}
if form.AllowCreateOrganization != nil {
ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
}
if form.ProhibitLogin != nil {
ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
}
if form.Restricted != nil {
ctx.ContextUser.IsRestricted = *form.Restricted
}
if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) ||
user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
opts := &user_service.UpdateOptions{
FullName: optional.FromPtr(form.FullName),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: optional.FromPtr(form.Admin),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
IsRestricted: optional.FromPtr(form.Restricted),
}
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if models.IsErrDeleteLastAdminUser(err) {
ctx.Error(http.StatusBadRequest, "LastAdmin", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
@@ -331,7 +301,8 @@ func DeleteUser(ctx *context.APIContext) {
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if models.IsErrUserOwnRepos(err) ||
models.IsErrUserHasOrgs(err) ||
models.IsErrUserOwnPackages(err) {
models.IsErrUserOwnPackages(err) ||
models.IsErrDeleteLastAdminUser(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteUser", err)
@@ -510,9 +481,6 @@ func RenameUser(ctx *context.APIContext) {
// Check if user name has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
switch {
case user_model.IsErrUsernameNotChanged(err):
// Noop as username is not changed
ctx.Status(http.StatusNoContent)
case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err):
@@ -528,5 +496,5 @@ func RenameUser(ctx *context.APIContext) {
}
log.Trace("User name changed: %s -> %s", oldName, newName)
ctx.Status(http.StatusOK)
ctx.Status(http.StatusNoContent)
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
)
// ListUserBadges lists all badges belonging to a user
func ListUserBadges(ctx *context.APIContext) {
// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
// ---
// summary: List a user's badges
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of user
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/BadgeList"
// "404":
// "$ref": "#/responses/notFound"
badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
return
}
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &badges)
}
// AddUserBadges add badges to a user
func AddUserBadges(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
// ---
// summary: Add a badge to a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of user
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UserBadgeOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.UserBadgeOption)
badges := prepareBadgesForReplaceOrAdd(ctx, *form)
if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteUserBadges delete a badge from a user
func DeleteUserBadges(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
// ---
// summary: Remove a badge from a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of user
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UserBadgeOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.UserBadgeOption)
badges := prepareBadgesForReplaceOrAdd(ctx, *form)
if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
return
}
ctx.Status(http.StatusNoContent)
}
func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
badges := make([]*user_model.Badge, len(form.BadgeSlugs))
for i, badge := range form.BadgeSlugs {
badges[i] = &user_model.Badge{
Slug: badge,
}
}
return badges
}
+141 -80
View File
@@ -2,13 +2,13 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package v1 Gitea API.
// Package v1 Gitea API
//
// This documentation describes the Gitea API.
//
// Schemes: http, https
// Schemes: https, http
// BasePath: /api/v1
// Version: {{AppVer | JSEscape | Safe}}
// Version: {{AppVer | JSEscape}}
// License: MIT http://opensource.org/licenses/MIT
//
// Consumes:
@@ -35,10 +35,12 @@
// type: apiKey
// name: token
// in: query
// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead.
// AccessToken:
// type: apiKey
// name: access_token
// in: query
// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead.
// AuthorizationHeaderToken:
// type: apiKey
// name: Authorization
@@ -77,7 +79,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -93,7 +94,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
@@ -316,10 +317,6 @@ func reqToken() func(ctx *context.APIContext) {
return
}
if ctx.IsBasicAuth {
ctx.CheckForOTP()
return
}
if ctx.IsSigned {
return
}
@@ -344,7 +341,6 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return
}
ctx.CheckForOTP()
}
}
@@ -701,12 +697,6 @@ func bind[T any](_ T) any {
}
}
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
// in the session (if there is a user id stored in session other plugins might return the user
// object for that id).
//
// The Session plugin is expected to be executed second, in order to skip authentication
// for users that have already signed in.
func buildAuthGroup() *auth.Group {
group := auth.NewGroup(
&auth.OAuth2{},
@@ -786,31 +776,6 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
})
return
}
if ctx.IsSigned && ctx.IsBasicAuth {
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
return // Skip 2FA
}
twofa, err := auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
if auth_model.IsErrTwoFactorNotEnrolled(err) {
return // No 2FA enrollment for this user
}
ctx.InternalServerError(err)
return
}
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
ok, err := twofa.ValidateTOTP(otpHeader)
if err != nil {
ctx.InternalServerError(err)
return
}
if !ok {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in user is allowed to call APIs.",
})
return
}
}
}
if options.AdminRequired {
@@ -824,6 +789,31 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
}
}
func individualPermsChecker(ctx *context.APIContext) {
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
if ctx.ContextUser.IsIndividual() {
switch {
case ctx.ContextUser.Visibility == api.VisibleTypePrivate:
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
ctx.NotFound("Visit Project", nil)
return
}
case ctx.ContextUser.Visibility == api.VisibleTypeLimited:
if ctx.Doer == nil {
ctx.NotFound("Visit Project", nil)
return
}
}
}
}
// check for and warn against deprecated authentication options
func checkDeprecatedAuthMethods(ctx *context.APIContext) {
if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
ctx.Resp.Header().Set("X-Gitea-Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
}
}
// Routes registers all v1 APIs routes to web application.
func Routes() *web.Route {
m := web.NewRoute()
@@ -831,9 +821,7 @@ func Routes() *web.Route {
m.Use(securityHeaders())
if setting.CORSConfig.Enabled {
m.Use(cors.Handler(cors.Options{
// Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
AllowedOrigins: setting.CORSConfig.AllowDomain,
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
AllowedOrigins: setting.CORSConfig.AllowDomain,
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...),
@@ -842,6 +830,8 @@ func Routes() *web.Route {
}
m.Use(context.APIContexter())
m.Use(checkDeprecatedAuthMethods)
// Get user from session if logged in.
m.Use(apiAuth(buildAuthGroup()))
@@ -864,11 +854,11 @@ func Routes() *web.Route {
m.Group("/user/{username}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context_service.UserAssignmentAPI())
}, context.UserAssignmentAPI())
m.Group("/user-id/{user-id}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context_service.UserIDAssignmentAPI())
}, context.UserIDAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
}
@@ -924,7 +914,7 @@ func Routes() *web.Route {
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
m.Get("/activities/feeds", user.ListUserActivityFeeds)
}, context_service.UserAssignmentAPI())
}, context.UserAssignmentAPI(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
// Users (requires user scope)
@@ -942,7 +932,7 @@ func Routes() *web.Route {
m.Get("/starred", user.GetStarredRepos)
m.Get("/subscriptions", user.GetWatchedRepos)
}, context_service.UserAssignmentAPI())
}, context.UserAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Users (requires user scope)
@@ -957,11 +947,26 @@ func Routes() *web.Route {
Post(bind(api.CreateEmailOption{}), user.AddEmail).
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
// create or update a user's actions secrets
m.Group("/actions/secrets", func() {
m.Combo("/{secretname}").
Put(bind(api.CreateOrUpdateSecretOption{}), user.CreateOrUpdateSecret).
Delete(repo.DeleteSecret)
// manage user-level actions features
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Combo("/{secretname}").
Put(bind(api.CreateOrUpdateSecretOption{}), user.CreateOrUpdateSecret).
Delete(user.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", user.ListVariables)
m.Combo("/{variablename}").
Get(user.GetVariable).
Delete(user.DeleteVariable).
Post(bind(api.CreateVariableOption{}), user.CreateVariable).
Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
})
})
m.Get("/followers", user.ListMyFollowers)
@@ -971,7 +976,7 @@ func Routes() *web.Route {
m.Get("", user.CheckMyFollowing)
m.Put("", user.Follow)
m.Delete("", user.Unfollow)
}, context_service.UserAssignmentAPI())
}, context.UserAssignmentAPI())
})
// (admin:public_key scope)
@@ -1031,7 +1036,16 @@ func Routes() *web.Route {
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
}, reqToken())
})
m.Group("/blocks", func() {
m.Get("", user.ListBlocks)
m.Group("/{username}", func() {
m.Get("", user.CheckUserBlock)
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope)
@@ -1061,10 +1075,25 @@ func Routes() *web.Route {
m.Post("/accept", repo.AcceptTransfer)
m.Post("/reject", repo.RejectTransfer)
}, reqToken())
m.Group("/actions/secrets", func() {
m.Combo("/{secretname}").
Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret).
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Combo("/{secretname}").
Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret).
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOwner(), repo.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOwner(), repo.GetVariable).
Delete(reqToken(), reqOwner(), repo.DeleteVariable).
Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
})
})
m.Group("/hooks/git", func() {
m.Combo("").Get(repo.ListGitHooks)
@@ -1156,9 +1185,9 @@ func Routes() *web.Route {
m.Get("/subscribers", repo.ListSubscribers)
m.Group("/subscription", func() {
m.Get("", user.IsWatching)
m.Put("", reqToken(), user.Watch)
m.Delete("", reqToken(), user.Unwatch)
})
m.Put("", user.Watch)
m.Delete("", user.Unwatch)
}, reqToken())
m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases).
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
@@ -1181,13 +1210,13 @@ func Routes() *web.Route {
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
})
}, reqRepoReader(unit.TypeReleases))
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), repo.PushMirrorSync)
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
m.Group("/push_mirrors", func() {
m.Combo("").Get(repo.ListPushMirrors).
Post(bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
Post(mustNotBeArchived, bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
m.Combo("/{name}").
Delete(repo.DeletePushMirrorByRemoteName).
Delete(mustNotBeArchived, repo.DeletePushMirrorByRemoteName).
Get(repo.GetPushMirrorByName)
}, reqAdmin(), reqToken())
@@ -1225,6 +1254,7 @@ func Routes() *web.Route {
Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
})
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
m.Group("/statuses", func() {
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
@@ -1235,6 +1265,7 @@ func Routes() *web.Route {
m.Group("/{ref}", func() {
m.Get("/status", repo.GetCombinedCommitStatusByRef)
m.Get("/statuses", repo.GetCommitStatusesByRef)
m.Get("/pull", repo.GetCommitPullRequest)
}, context.ReferencesGitRepo())
}, reqRepoReader(unit.TypeCode))
m.Group("/git", func() {
@@ -1298,8 +1329,8 @@ func Routes() *web.Route {
m.Group("/{username}/{reponame}", func() {
m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
m.Get("/pinned", repo.ListPinnedIssues)
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
m.Group("/comments", func() {
m.Get("", repo.ListRepoIssueComments)
m.Group("/{id}", func() {
@@ -1413,14 +1444,14 @@ func Routes() *web.Route {
m.Get("/files", reqToken(), packages.ListPackageFiles)
})
m.Get("/", reqToken(), packages.ListPackages)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Group("/orgs/{org}", func() {
@@ -1434,11 +1465,26 @@ func Routes() *web.Route {
m.Combo("/{username}").Get(reqToken(), org.IsMember).
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
})
m.Group("/actions/secrets", func() {
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret).
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret).
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOrgOwnership(), org.GetVariable).
Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
})
})
m.Group("/public_members", func() {
m.Get("", org.ListPublicMembers)
@@ -1447,10 +1493,10 @@ func Routes() *web.Route {
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
})
m.Group("/teams", func() {
m.Get("", reqToken(), org.ListTeams)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", reqToken(), org.SearchTeam)
}, reqOrgMembership())
m.Get("", org.ListTeams)
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", org.SearchTeam)
}, reqToken(), reqOrgMembership())
m.Group("/labels", func() {
m.Get("", org.ListLabels)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
@@ -1470,6 +1516,15 @@ func Routes() *web.Route {
m.Delete("", org.DeleteAvatar)
}, reqToken(), reqOrgOwnership())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
m.Group("/blocks", func() {
m.Get("", org.ListBlocks)
m.Group("/{username}", func() {
m.Get("", org.CheckUserBlock)
m.Put("", org.BlockUser)
m.Delete("", org.UnblockUser)
})
}, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
@@ -1512,7 +1567,10 @@ func Routes() *web.Route {
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
}, context_service.UserAssignmentAPI())
m.Get("/badges", admin.ListUserBadges)
m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
}, context.UserAssignmentAPI())
})
m.Group("/emails", func() {
m.Get("", admin.GetAllEmails)
@@ -1530,6 +1588,9 @@ func Routes() *web.Route {
Patch(bind(api.EditHookOption{}), admin.EditHook).
Delete(admin.DeleteHook)
})
m.Group("/runners", func() {
m.Get("/registration-token", admin.GetRegistrationToken)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
m.Group("/topics", func() {
+1 -1
View File
@@ -6,11 +6,11 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/options"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
// Shows a list of all Gitignore templates
+1 -1
View File
@@ -6,9 +6,9 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/context"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+1 -1
View File
@@ -8,12 +8,12 @@ import (
"net/http"
"net/url"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/options"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
// Returns a list of all License templates
+1 -1
View File
@@ -6,12 +6,12 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
)
// Markup render markup document to HTML
+10 -10
View File
@@ -10,19 +10,19 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
const (
AppURL = "http://localhost:3000/"
Repo = "gogits/gogs"
AppSubURL = AppURL + Repo + "/"
AppURL = "http://localhost:3000/"
Repo = "gogits/gogs"
FullURL = AppURL + Repo + "/"
)
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
@@ -74,20 +74,20 @@ func TestAPI_RenderGFM(t *testing.T) {
// rendered
`<p>Wiki! Enjoy :)</p>
<ul>
<li><a href="` + AppSubURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + AppSubURL + `wiki/Tips" rel="nofollow">Tips</a></li>
<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
</ul>
`,
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
`,
// special syntax
`[[Name|Link]]`,
// rendered
`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
`,
// empty
``,
@@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
<h2 id="user-content-quick-links">Quick Links</h2>
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
`,
}
+8 -10
View File
@@ -9,9 +9,9 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
)
const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
@@ -29,10 +29,9 @@ func NodeInfo(ctx *context.APIContext) {
nodeInfoUsage := structs.NodeInfoUsage{}
if setting.Federation.ShareUserStatistics {
cached := false
if setting.CacheService.Enabled {
nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage)
}
var cached bool
nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage)
if !cached {
usersTotal := int(user_model.CountUsers(ctx, nil))
now := time.Now()
@@ -53,11 +52,10 @@ func NodeInfo(ctx *context.APIContext) {
LocalPosts: int(allIssues),
LocalComments: int(allComments),
}
if setting.CacheService.Enabled {
if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
ctx.InternalServerError(err)
return
}
if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
ctx.InternalServerError(err)
return
}
}
}
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/modules/context"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
)
// SigningKey returns the public key of the default signing key if it exists
+1 -1
View File
@@ -6,9 +6,9 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
)
// Version shows the version of the Gitea server
+13 -2
View File
@@ -8,9 +8,10 @@ import (
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
)
// NewAvailable check if unread notifications exist
@@ -21,7 +22,17 @@ func NewAvailable(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/NotificationCount"
ctx.JSON(http.StatusOK, api.NotificationCount{New: activities_model.CountUnread(ctx, ctx.Doer.ID)})
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID,
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
})
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "db.Count[activities_model.Notification]", err)
return
}
ctx.JSON(http.StatusOK, api.NotificationCount{New: total})
}
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
+6 -5
View File
@@ -9,9 +9,10 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -108,18 +109,18 @@ func ListRepoNotifications(ctx *context.APIContext) {
}
opts.RepoID = ctx.Repo.Repository.ID
totalCount, err := activities_model.CountNotifications(ctx, opts)
totalCount, err := db.Count[activities_model.Notification](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
nl, err := activities_model.GetNotifications(ctx, opts)
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
err = nl.LoadAttributes(ctx)
err = activities_model.NotificationList(nl).LoadAttributes(ctx)
if err != nil {
ctx.InternalServerError(err)
return
@@ -202,7 +203,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
log.Error("%v", opts.Status)
}
nl, err := activities_model.GetNotifications(ctx, opts)
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
+1 -1
View File
@@ -10,7 +10,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+6 -5
View File
@@ -8,8 +8,9 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -68,18 +69,18 @@ func ListNotifications(ctx *context.APIContext) {
return
}
totalCount, err := activities_model.CountNotifications(ctx, opts)
totalCount, err := db.Count[activities_model.Notification](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
nl, err := activities_model.GetNotifications(ctx, opts)
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
err = nl.LoadAttributes(ctx)
err = activities_model.NotificationList(nl).LoadAttributes(ctx)
if err != nil {
ctx.InternalServerError(err)
return
@@ -147,7 +148,7 @@ func ReadNotifications(ctx *context.APIContext) {
statuses := ctx.FormStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
}
nl, err := activities_model.GetNotifications(ctx, opts)
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"encoding/base64"
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2024 The Gitea Authors.
// SPDX-License-Identifier: MIT
package org
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
func ListBlocks(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
// ---
// summary: List users blocked by the organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/UserList"
shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
}
func CheckUserBlock(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
// ---
// summary: Check if a user is blocked by the organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: user to check
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
}
func BlockUser(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
// ---
// summary: Block a user
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: user to block
// type: string
// required: true
// - name: note
// in: query
// description: optional note for the block
// type: string
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
}
func UnblockUser(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
// ---
// summary: Unblock a user
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: user to unblock
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
}
+1 -1
View File
@@ -6,10 +6,10 @@ package org
import (
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
)
+1 -1
View File
@@ -9,11 +9,11 @@ import (
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+2 -2
View File
@@ -9,11 +9,11 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
}
ctx.Status(http.StatusNoContent)
+23 -24
View File
@@ -12,13 +12,15 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org"
user_service "code.gitea.io/gitea/services/user"
)
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
@@ -30,14 +32,9 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
UserID: u.ID,
IncludePrivate: showPrivate,
}
orgs, err := organization.FindOrgs(ctx, opts)
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindOrgs", err)
return
}
maxResults, err := organization.CountOrgs(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CountOrgs", err)
ctx.Error(http.StatusInternalServerError, "db.FindAndCount[organization.Organization]", err)
return
}
@@ -342,28 +339,30 @@ func Edit(ctx *context.APIContext) {
// "$ref": "#/responses/Organization"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditOrgOption)
org := ctx.Org.Organization
org.FullName = form.FullName
org.Email = form.Email
org.Description = form.Description
org.Website = form.Website
org.Location = form.Location
if form.Visibility != "" {
org.Visibility = api.VisibilityModes[form.Visibility]
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
return
}
}
if form.RepoAdminChangeTeamAccess != nil {
org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_model.UpdateUserCols(ctx, org.AsUser(),
"full_name", "description", "website", "location",
"visibility", "repo_admin_change_team_access",
); err != nil {
ctx.Error(http.StatusInternalServerError, "EditOrganization", err)
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
return
}
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
}
// Delete an organization
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register org runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
@@ -7,12 +7,13 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -48,13 +49,7 @@ func ListActionsSecrets(ctx *context.APIContext) {
ListOptions: utils.GetListOptions(ctx),
}
count, err := secret_model.CountSecrets(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
secrets, err := secret_model.FindSecrets(ctx, *opts)
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
+11 -4
View File
@@ -15,12 +15,13 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
ctx.Error(http.StatusInternalServerError, "AddMember", err)
if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "AddTeamMember", err)
} else {
ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
}
return
}
ctx.Status(http.StatusNoContent)
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
return
}
if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
return
}
+291
View File
@@ -0,0 +1,291 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
)
// ListVariables list org-level variables
func ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
// ---
// summary: Get an org-level variables list
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/VariableList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
return
}
variables := make([]*api.ActionVariable, len(vars))
for i, v := range vars {
variables[i] = &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
// GetVariable get an org-level variable
func GetVariable(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
// ---
// summary: Get an org-level variable
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
variable := &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
ctx.JSON(http.StatusOK, variable)
}
// DeleteVariable delete an org-level variable
func DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
// ---
// summary: Delete an org-level variable
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "201":
// description: response when deleting a variable
// "204":
// description: response when deleting a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// CreateVariable create an org-level variable
func CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
// ---
// summary: Create an org-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariableOption"
// responses:
// "201":
// description: response when creating an org-level variable
// "204":
// description: response when creating an org-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateVariableOption)
ownerID := ctx.Org.Organization.ID
variableName := ctx.Params("variablename")
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ownerID,
Name: variableName,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
return
}
if v != nil && v.ID > 0 {
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
return
}
if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// UpdateVariable update an org-level variable
func UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
// ---
// summary: Update an org-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateVariableOption"
// responses:
// "201":
// description: response when updating an org-level variable
// "204":
// description: response when updating an org-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
if opt.Name == "" {
opt.Name = ctx.Params("variablename")
}
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+3 -3
View File
@@ -7,10 +7,10 @@ import (
"net/http"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) {
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
IsInternal: util.OptionalBoolFalse,
IsInternal: optional.Some(false),
Paginator: &listOptions,
})
if err != nil {
+297 -1
View File
@@ -7,10 +7,14 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/context"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
// GetVariable get a repo-level variable
func GetVariable(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
// ---
// summary: Get a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
variable := &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
}
ctx.JSON(http.StatusOK, variable)
}
// DeleteVariable delete a repo-level variable
func DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
// ---
// summary: Delete a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "201":
// description: response when deleting a variable
// "204":
// description: response when deleting a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// CreateVariable create a repo-level variable
func CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
// ---
// summary: Create a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariableOption"
// responses:
// "201":
// description: response when creating a repo-level variable
// "204":
// description: response when creating a repo-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateVariableOption)
repoID := ctx.Repo.Repository.ID
variableName := ctx.Params("variablename")
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
RepoID: repoID,
Name: variableName,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
return
}
if v != nil && v.ID > 0 {
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
return
}
if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// UpdateVariable update a repo-level variable
func UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
// ---
// summary: Update a repo-level variable
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateVariableOption"
// responses:
// "201":
// description: response when updating a repo-level variable
// "204":
// description: response when updating a repo-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.Params("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
}
return
}
if opt.Name == "" {
opt.Name = ctx.Params("variablename")
}
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// ListVariables list repo-level variables
func ListVariables(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
// ---
// summary: Get repo-level variables list
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: name of the owner
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/VariableList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
ListOptions: utils.GetListOptions(ctx),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
return
}
variables := make([]*api.ActionVariable, len(vars))
for i, v := range vars {
variables[i] = &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"encoding/base64"
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
+1 -1
View File
@@ -6,7 +6,7 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
)
+18 -12
View File
@@ -10,16 +10,18 @@ import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
@@ -137,9 +139,9 @@ func DeleteBranch(ctx *context.APIContext) {
}
// check whether branches of this repository has been synced
totalNumOfBranches, err := git_model.CountBranches(ctx, git_model.FindBranchOptions{
totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: util.OptionalBoolFalse,
IsDeletedBranch: optional.Some(false),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "CountBranches", err)
@@ -251,12 +253,11 @@ func CreateBranch(ctx *context.APIContext) {
}
}
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName)
if err != nil {
if git_model.IsErrBranchNotExist(err) {
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
}
if models.IsErrTagAlreadyExists(err) {
} else if models.IsErrTagAlreadyExists(err) {
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")
} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
ctx.Error(http.StatusConflict, "", "The branch already exists.")
@@ -339,10 +340,10 @@ func ListBranches(ctx *context.APIContext) {
branchOpts := git_model.FindBranchOptions{
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: util.OptionalBoolFalse,
IsDeletedBranch: optional.Some(false),
}
var err error
totalNumOfBranches, err = git_model.CountBranches(ctx, branchOpts)
totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CountBranches", err)
return
@@ -361,7 +362,7 @@ func ListBranches(ctx *context.APIContext) {
return
}
branches, err := git_model.FindBranches(ctx, branchOpts)
branches, err := db.Find[git_model.Branch](ctx, branchOpts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranches", err)
return
@@ -615,6 +616,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
DismissStaleApprovals: form.DismissStaleApprovals,
IgnoreStaleApprovals: form.IgnoreStaleApprovals,
RequireSignedCommits: form.RequireSignedCommits,
ProtectedFilePatterns: form.ProtectedFilePatterns,
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
@@ -642,7 +644,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
} else {
if !isPlainRule {
if ctx.Repo.GitRepo == nil {
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return
@@ -786,6 +788,10 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals
}
if form.IgnoreStaleApprovals != nil {
protectBranch.IgnoreStaleApprovals = *form.IgnoreStaleApprovals
}
if form.RequireSignedCommits != nil {
protectBranch.RequireSignedCommits = *form.RequireSignedCommits
}
@@ -915,7 +921,7 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
if !isPlainRule {
if ctx.Repo.GitRepo == nil {
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return
+14 -11
View File
@@ -12,11 +12,11 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -53,13 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
count, err := repo_model.CountCollaborators(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.InternalServerError(err)
return
}
collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return
@@ -70,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
}
ctx.SetTotalCountHeader(count)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, users)
}
@@ -156,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
@@ -179,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
}
if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "AddCollaborator", err)
} else {
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
}
return
}
@@ -234,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
return
}
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
return
}
+52 -3
View File
@@ -10,12 +10,13 @@ import (
"net/http"
"strconv"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "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"
)
@@ -205,7 +206,6 @@ func GetAllCommits(ctx *context.APIContext) {
Not: not,
Revision: []string{baseCommit.ID.String()},
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommitsCount", err)
return
@@ -245,7 +245,6 @@ func GetAllCommits(ctx *context.APIContext) {
Not: not,
Page: listOptions.Page,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err)
return
@@ -325,3 +324,53 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
return
}
}
// GetCommitPullRequest returns the pull request of the commit
func GetCommitPullRequest(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
// ---
// summary: Get the pull request of the commit
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: SHA of the commit to get
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
// "404":
// "$ref": "#/responses/notFound"
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.Params(":sha"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
}
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}
+9 -8
View File
@@ -19,8 +19,8 @@ import (
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -29,6 +29,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
files_service "code.gitea.io/gitea/services/repository/files"
)
@@ -144,7 +145,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return
}
// OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
// OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
dataRc, err := blob.DataAsync()
if err != nil {
ctx.ServerError("DataAsync", err)
@@ -279,9 +280,8 @@ func GetArchive(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoPath := repo_model.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
if ctx.Repo.GitRepo == nil {
gitRepo, err := git.OpenRepository(ctx, repoPath)
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return
@@ -408,7 +408,7 @@ func canReadFiles(r *context.Repository) bool {
return r.Permission.CanRead(unit.TypeCode)
}
func base64Reader(s string) (io.Reader, error) {
func base64Reader(s string) (io.ReadSeeker, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
@@ -655,6 +655,7 @@ func UpdateFile(ctx *context.APIContext) {
apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
if ctx.Repo.Repository.IsEmpty {
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
return
}
if apiOpts.BranchName == "" {
@@ -762,13 +763,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
}
message := ""
if len(createFiles) != 0 {
message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
}
if len(updateFiles) != 0 {
message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
}
if len(deleteFiles) != 0 {
message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
}
return strings.Trim(message, "\n")
}
+3 -1
View File
@@ -14,11 +14,11 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
if err != nil {
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
ctx.Error(http.StatusConflict, "ForkRepository", err)
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "ForkRepository", err)
} else {
ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
}
+1 -1
View File
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+1 -1
View File
@@ -7,10 +7,10 @@ import (
"net/http"
"net/url"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
)
// GetGitAllRefs get ref or an list all the refs of a repository
+3 -8
View File
@@ -7,16 +7,17 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
webhook_service "code.gitea.io/gitea/services/webhook"
)
@@ -58,13 +59,7 @@ func ListHooks(ctx *context.APIContext) {
RepoID: ctx.Repo.Repository.ID,
}
count, err := webhook.CountWebhooksByOpts(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
hooks, err := webhook.ListWebhooksByOpts(ctx, opts)
hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
+69 -40
View File
@@ -5,6 +5,7 @@
package repo
import (
"errors"
"fmt"
"net/http"
"strconv"
@@ -18,14 +19,14 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
@@ -122,14 +123,14 @@ func SearchIssues(ctx *context.APIContext) {
return
}
var isClosed util.OptionalBool
var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isClosed = util.OptionalBoolTrue
isClosed = optional.Some(true)
case "all":
isClosed = util.OptionalBoolNone
isClosed = optional.None[bool]()
default:
isClosed = util.OptionalBoolFalse
isClosed = optional.Some(false)
}
var (
@@ -142,7 +143,7 @@ func SearchIssues(ctx *context.APIContext) {
Private: false,
AllPublic: true,
TopicOnly: false,
Collaborate: util.OptionalBoolNone,
Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
@@ -165,7 +166,7 @@ func SearchIssues(ctx *context.APIContext) {
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
opts.Collaborate = util.OptionalBoolFalse
opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
@@ -204,14 +205,14 @@ func SearchIssues(ctx *context.APIContext) {
keyword = ""
}
var isPull util.OptionalBool
var isPull optional.Option[bool]
switch ctx.FormString("type") {
case "pulls":
isPull = util.OptionalBoolTrue
isPull = optional.Some(true)
case "issues":
isPull = util.OptionalBoolFalse
isPull = optional.Some(false)
default:
isPull = util.OptionalBoolNone
isPull = optional.None[bool]()
}
var includedAnyLabels []int64
@@ -268,28 +269,28 @@ func SearchIssues(ctx *context.APIContext) {
}
if since != 0 {
searchOpt.UpdatedAfterUnix = &since
searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
searchOpt.UpdatedBeforeUnix = &before
searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
searchOpt.PosterID = &ctxUserID
searchOpt.PosterID = optional.Some(ctxUserID)
}
if ctx.FormBool("assigned") {
searchOpt.AssigneeID = &ctxUserID
searchOpt.AssigneeID = optional.Some(ctxUserID)
}
if ctx.FormBool("mentioned") {
searchOpt.MentionID = &ctxUserID
searchOpt.MentionID = optional.Some(ctxUserID)
}
if ctx.FormBool("review_requested") {
searchOpt.ReviewRequestedID = &ctxUserID
searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
}
if ctx.FormBool("reviewed") {
searchOpt.ReviewedID = &ctxUserID
searchOpt.ReviewedID = optional.Some(ctxUserID)
}
}
@@ -367,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
// required: false
// - name: created_by
// in: query
// description: Only show items which were created by the the given user
// description: Only show items which were created by the given user
// type: string
// - name: assigned_by
// in: query
@@ -396,14 +397,14 @@ func ListIssues(ctx *context.APIContext) {
return
}
var isClosed util.OptionalBool
var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isClosed = util.OptionalBoolTrue
isClosed = optional.Some(true)
case "all":
isClosed = util.OptionalBoolNone
isClosed = optional.None[bool]()
default:
isClosed = util.OptionalBoolFalse
isClosed = optional.Some(false)
}
keyword := ctx.FormTrim("q")
@@ -452,14 +453,30 @@ func ListIssues(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
var isPull util.OptionalBool
isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
isPull = util.OptionalBoolTrue
isPull = optional.Some(true)
case "issues":
isPull = util.OptionalBoolFalse
default:
isPull = util.OptionalBoolNone
isPull = optional.Some(false)
}
if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
ctx.NotFound()
return
}
if !isPull.Has() {
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
if !canReadIssues && !canReadPulls {
ctx.NotFound()
return
} else if !canReadIssues {
isPull = optional.Some(true)
} else if !canReadPulls {
isPull = optional.Some(false)
}
}
// FIXME: we should be more efficient here
@@ -485,10 +502,10 @@ func ListIssues(ctx *context.APIContext) {
SortBy: issue_indexer.SortByCreatedDesc,
}
if since != 0 {
searchOpt.UpdatedAfterUnix = &since
searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
searchOpt.UpdatedBeforeUnix = &before
searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if len(labelIDs) == 1 && labelIDs[0] == 0 {
searchOpt.NoLabelOnly = true
@@ -509,13 +526,13 @@ func ListIssues(ctx *context.APIContext) {
}
if createdByID > 0 {
searchOpt.PosterID = &createdByID
searchOpt.PosterID = optional.Some(createdByID)
}
if assignedByID > 0 {
searchOpt.AssigneeID = &assignedByID
searchOpt.AssigneeID = optional.Some(assignedByID)
}
if mentionedByID > 0 {
searchOpt.MentionID = &mentionedByID
searchOpt.MentionID = optional.Some(mentionedByID)
}
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
@@ -593,6 +610,10 @@ func GetIssue(ctx *context.APIContext) {
}
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
}
@@ -633,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateIssueOption)
var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
@@ -687,12 +709,14 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0)
}
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
return
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "NewIssue", err)
} else {
ctx.Error(http.StatusInternalServerError, "NewIssue", err)
}
ctx.Error(http.StatusInternalServerError, "NewIssue", err)
return
}
@@ -828,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
}
return
}
}
@@ -844,10 +872,11 @@ func EditIssue(ctx *context.APIContext) {
}
if form.State != nil {
if issue.IsPull {
if pr, err := issue.GetPullRequest(ctx); err != nil {
if err := issue.LoadPullRequest(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
return
} else if pr.HasMerged {
}
if issue.PullRequest.HasMerged {
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
return
}
+1 -1
View File
@@ -8,12 +8,12 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
+68 -10
View File
@@ -12,11 +12,13 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -71,6 +73,11 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
issue.Repo = ctx.Repo.Repository
opts := &issues_model.FindCommentsOptions{
@@ -271,12 +278,27 @@ func ListRepoIssueComments(ctx *context.APIContext) {
return
}
var isPull optional.Option[bool]
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
if canReadIssue && canReadPull {
isPull = optional.None[bool]()
} else if canReadIssue {
isPull = optional.Some(false)
} else if canReadPull {
isPull = optional.Some(true)
} else {
ctx.NotFound()
return
}
opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
Type: issues_model.CommentTypeComment,
Since: since,
Before: before,
IsPull: isPull,
}
comments, err := issues_model.FindComments(ctx, opts)
@@ -301,10 +323,6 @@ func ListRepoIssueComments(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
return
}
if err := comments.LoadPosters(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
return
}
if err := comments.LoadAttachments(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
return
@@ -360,6 +378,7 @@ func CreateIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@@ -367,14 +386,23 @@ func CreateIssueComment(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.NotFound()
return
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
return
}
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
}
return
}
@@ -436,6 +464,11 @@ func GetIssueComment(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
}
if comment.Type != issues_model.CommentTypeComment {
ctx.Status(http.StatusNoContent)
return
@@ -490,6 +523,7 @@ func EditIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
editIssueComment(ctx, *form)
}
@@ -555,7 +589,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
}
@@ -568,7 +612,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
oldContent := comment.Content
comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "UpdateComment", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
}
return
}
@@ -658,7 +706,17 @@ func deleteIssueComment(ctx *context.APIContext) {
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
} else if comment.Type != issues_model.CommentTypeComment {
@@ -4,16 +4,18 @@
package repo
import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/error"
// "423":
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
ctx.ServerError("UpdateComment", err)
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, "UpdateComment", err)
} else {
ctx.ServerError("UpdateComment", err)
}
return
}
@@ -329,6 +337,10 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
return nil
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
return nil
}
comment.Issue.Repo = ctx.Repo.Repository
return comment
+33 -30
View File
@@ -11,10 +11,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -102,23 +102,24 @@ func GetIssueDependencies(ctx *context.APIContext) {
return
}
var lastRepoID int64
var lastPerm access_model.Permission
repoPerms := make(map[int64]access_model.Permission)
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
for _, blocker := range blockersInfo {
// Get the permissions for this repository
perm := lastPerm
if lastRepoID != blocker.Repository.ID {
if blocker.Repository.ID == ctx.Repo.Repository.ID {
perm = ctx.Repo.Permission
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
// If the repo ID exists in the map, return the exist permissions
// else get the permission and add it to the map
var perm access_model.Permission
existPerm, ok := repoPerms[blocker.RepoID]
if ok {
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
lastRepoID = blocker.Repository.ID
repoPerms[blocker.RepoID] = perm
}
// check permission
@@ -345,29 +346,31 @@ func GetIssueBlocks(ctx *context.APIContext) {
return
}
var lastRepoID int64
var lastPerm access_model.Permission
var issues []*issues_model.Issue
repoPerms := make(map[int64]access_model.Permission)
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
for i, depMeta := range deps {
if i < skip || i >= max {
continue
}
// Get the permissions for this repository
perm := lastPerm
if lastRepoID != depMeta.Repository.ID {
if depMeta.Repository.ID == ctx.Repo.Repository.ID {
perm = ctx.Repo.Permission
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
// If the repo ID exists in the map, return the exist permissions
// else get the permission and add it to the map
var perm access_model.Permission
existPerm, ok := repoPerms[depMeta.RepoID]
if ok {
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
lastRepoID = depMeta.Repository.ID
repoPerms[depMeta.RepoID] = perm
}
if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
+1 -1
View File
@@ -8,9 +8,9 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
+6 -12
View File
@@ -7,8 +7,8 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -240,18 +240,12 @@ func ListPinnedPullRequests(ctx *context.APIContext) {
}
apiPrs := make([]*api.PullRequest, len(issues))
if err := issues.LoadPullRequests(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err)
return
}
for i, currentIssue := range issues {
pr, err := currentIssue.GetPullRequest(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
return
}
if err = pr.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
pr := currentIssue.PullRequest
if err = pr.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
+26 -8
View File
@@ -8,11 +8,13 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
// GetIssueCommentReactions list reactions of a comment from an issue
@@ -61,6 +63,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
@@ -190,9 +198,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
return
}
err = comment.LoadIssue(ctx)
if err != nil {
if err = comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
}
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
@@ -202,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
if isCreateType {
// PostIssueCommentReaction part
reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) {
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, err.Error(), err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
@@ -418,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
if isCreateType {
// PostIssueReaction part
reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) {
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, err.Error(), err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
@@ -429,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
Created: reaction.CreatedUnix.AsTime(),
})
} else {
ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
}
return
}
+1 -1
View File
@@ -8,8 +8,8 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+1 -1
View File
@@ -9,9 +9,9 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
api "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"
)
+1 -1
View File
@@ -12,10 +12,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+9 -9
View File
@@ -15,12 +15,12 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -83,20 +83,14 @@ func ListDeployKeys(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
opts := &asymkey_model.ListDeployKeysOptions{
opts := asymkey_model.ListDeployKeysOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
KeyID: ctx.FormInt64("key_id"),
Fingerprint: ctx.FormString("fingerprint"),
}
keys, err := asymkey_model.ListDeployKeys(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
count, err := asymkey_model.CountDeployKeys(ctx, opts)
keys, count, err := db.FindAndCount[asymkey_model.DeployKey](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -159,6 +153,12 @@ func GetDeployKey(ctx *context.APIContext) {
return
}
// this check make it more consistent
if key.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if err = key.GetContent(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "GetContent", err)
return
+1 -1
View File
@@ -9,11 +9,11 @@ import (
"strconv"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
+1 -1
View File
@@ -9,8 +9,8 @@ import (
"strconv"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
)
type languageResponse []*repo_model.LanguageStat
+1 -1
View File
@@ -17,7 +17,6 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -26,6 +25,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"

Some files were not shown because too many files have changed in this diff Show More