Compare commits
8 Commits
master
...
artifacts-
Author | SHA1 | Date |
---|---|---|
Christopher Homberger | de481dde3b | |
Christopher Homberger | ad925c2eca | |
Christopher Homberger | debdad9684 | |
Christopher Homberger | b511927642 | |
Christopher Homberger | 9c3c3a9a9a | |
Christopher Homberger | f53d8b1933 | |
Christopher Homberger | 325781acc6 | |
Christopher Homberger | dbd3b7333a |
5
go.mod
5
go.mod
|
@ -37,6 +37,11 @@ require (
|
|||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
google.golang.org/protobuf v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
|
|
7
go.sum
7
go.sum
|
@ -65,10 +65,14 @@ github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3c
|
|||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
|
@ -277,6 +281,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
|
|
@ -0,0 +1,442 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package artifacts
|
||||
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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 WriteFS
|
||||
rfs fs.FS
|
||||
AppURL string
|
||||
baseDir string
|
||||
}
|
||||
|
||||
type ArtifactContext struct {
|
||||
Req *http.Request
|
||||
Resp http.ResponseWriter
|
||||
}
|
||||
|
||||
func (c ArtifactContext) Error(status int, _ ...interface{}) {
|
||||
c.Resp.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (c ArtifactContext) JSON(status int, _ ...interface{}) {
|
||||
c.Resp.WriteHeader(status)
|
||||
}
|
||||
|
||||
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (interface{}, int64, bool) {
|
||||
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 nil, runID, true
|
||||
}
|
||||
|
||||
func RoutesV4(router *httprouter.Router, baseDir string, fsys WriteFS, rfs fs.FS) {
|
||||
route := &artifactV4Routes{
|
||||
fs: fsys,
|
||||
rfs: rfs,
|
||||
baseDir: baseDir,
|
||||
prefix: ArtifactV4RouteBase,
|
||||
}
|
||||
router.POST(path.Join(ArtifactV4RouteBase, "CreateArtifact"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.AppURL = r.Host
|
||||
route.createArtifact(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
router.POST(path.Join(ArtifactV4RouteBase, "FinalizeArtifact"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.finalizeArtifact(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
router.POST(path.Join(ArtifactV4RouteBase, "ListArtifacts"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.listArtifacts(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
router.POST(path.Join(ArtifactV4RouteBase, "GetSignedArtifactURL"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.AppURL = r.Host
|
||||
route.getSignedArtifactURL(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
router.POST(path.Join(ArtifactV4RouteBase, "DeleteArtifact"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.AppURL = r.Host
|
||||
route.deleteArtifact(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
router.PUT(path.Join(ArtifactV4RouteBase, "UploadArtifact"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.uploadArtifact(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
router.GET(path.Join(ArtifactV4RouteBase, "DownloadArtifact"), func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
route.downloadArtifact(&ArtifactContext{
|
||||
Req: r,
|
||||
Resp: w,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
|
||||
mac := hmac.New(sha256.New, []byte{0xba, 0xdb, 0xee, 0xf0})
|
||||
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 := "http://" + strings.TrimSuffix(r.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) (int64, 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 -1, "", 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 -1, "", false
|
||||
}
|
||||
return taskID, artifactName, true
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
log.Errorf("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.Errorf("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.Errorf("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
|
||||
}
|
||||
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
artifactName := req.Name
|
||||
|
||||
respData := CreateArtifactResponse{
|
||||
Ok: true,
|
||||
SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, runID),
|
||||
}
|
||||
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":
|
||||
|
||||
safeRunPath := safeResolve(r.baseDir, fmt.Sprint(task))
|
||||
safePath := safeResolve(safeRunPath, artifactName)
|
||||
safePath = safeResolve(safePath, artifactName+".zip")
|
||||
|
||||
file, err := func() (WritableFile, error) {
|
||||
if comp == "appendBlock" {
|
||||
return r.fs.OpenAppendable(safePath)
|
||||
}
|
||||
return r.fs.OpenWritable(safePath)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer, ok := file.(io.Writer)
|
||||
if !ok {
|
||||
panic(errors.New("File is not writable"))
|
||||
}
|
||||
|
||||
if ctx.Req.Body == nil {
|
||||
panic(errors.New("No body given"))
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, ctx.Req.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
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
|
||||
}
|
||||
_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
respData := FinalizeArtifactResponse{
|
||||
Ok: true,
|
||||
ArtifactId: 1,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
safePath := safeResolve(r.baseDir, fmt.Sprint(runID))
|
||||
|
||||
entries, err := fs.ReadDir(r.rfs, safePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
list := []*ListArtifactsResponse_MonolithArtifact{}
|
||||
|
||||
for _, entry := range entries {
|
||||
if req.NameFilter == nil || req.NameFilter.Value == entry.Name() {
|
||||
data := &ListArtifactsResponse_MonolithArtifact{
|
||||
Name: entry.Name(),
|
||||
CreatedAt: timestamppb.Now(),
|
||||
DatabaseId: 1,
|
||||
WorkflowRunBackendId: req.WorkflowRunBackendId,
|
||||
WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
|
||||
Size: 0,
|
||||
}
|
||||
if info, err := entry.Info(); err == nil {
|
||||
data.Size = info.Size()
|
||||
data.CreatedAt = timestamppb.New(info.ModTime())
|
||||
}
|
||||
list = append(list, data)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
respData := GetSignedArtifactURLResponse{}
|
||||
|
||||
respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, runID)
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
|
||||
task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
safeRunPath := safeResolve(r.baseDir, fmt.Sprint(task))
|
||||
safePath := safeResolve(safeRunPath, artifactName)
|
||||
safePath = safeResolve(safePath, artifactName+".zip")
|
||||
|
||||
file, _ := r.rfs.Open(safePath)
|
||||
|
||||
_, _ = 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
|
||||
}
|
||||
safeRunPath := safeResolve(r.baseDir, fmt.Sprint(runID))
|
||||
safePath := safeResolve(safeRunPath, req.Name)
|
||||
|
||||
_ = os.RemoveAll(safePath)
|
||||
|
||||
respData := DeleteArtifactResponse{
|
||||
Ok: true,
|
||||
ArtifactId: 1,
|
||||
}
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -289,6 +289,7 @@ func Serve(ctx context.Context, artifactPath string, addr string, port string) c
|
|||
fsys := readWriteFSImpl{}
|
||||
uploads(router, artifactPath, fsys)
|
||||
downloads(router, artifactPath, fsys)
|
||||
RoutesV4(router, artifactPath, fsys, fsys)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", addr, port),
|
||||
|
|
|
@ -262,6 +262,7 @@ func TestArtifactFlow(t *testing.T) {
|
|||
tables := []TestJobFileInfo{
|
||||
{"testdata", "upload-and-download", "push", "", platforms, ""},
|
||||
{"testdata", "GHSL-2023-004", "push", "", platforms, ""},
|
||||
{"testdata", "v4", "push", "", platforms, ""},
|
||||
}
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
on:
|
||||
push:
|
||||
jobs:
|
||||
_5:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: env
|
||||
- run: |
|
||||
github:
|
||||
${{ tojson(github) }}
|
||||
inputs:
|
||||
${{ tojson(inputs) }}
|
||||
matrix:
|
||||
${{ tojson(matrix) }}
|
||||
needs:
|
||||
${{ tojson(needs) }}
|
||||
strategy:
|
||||
${{ tojson(strategy) }}
|
||||
shell: cp {0} context.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test
|
||||
path: context.txt
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test
|
||||
path: out
|
||||
- run: cat out/context.txt
|
||||
- run: |
|
||||
No content
|
||||
shell: cp {0} context.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test
|
||||
path: context.txt
|
||||
overwrite: true
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test
|
||||
path: out2
|
||||
- run: cat out2/context.txt
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type actionsClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
Scp string `json:"scp"`
|
||||
TaskID int64
|
||||
RunID int64
|
||||
JobID int64
|
||||
Ac string `json:"ac"`
|
||||
}
|
||||
|
||||
type actionsCacheScope struct {
|
||||
Scope string
|
||||
Permission actionsCachePermission
|
||||
}
|
||||
|
||||
type actionsCachePermission int
|
||||
|
||||
const (
|
||||
actionsCachePermissionRead = 1 << iota
|
||||
actionsCachePermissionWrite
|
||||
)
|
||||
|
||||
func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
ac, err := json.Marshal(&[]actionsCacheScope{
|
||||
{
|
||||
Scope: "",
|
||||
Permission: actionsCachePermissionWrite,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := actionsClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
},
|
||||
Scp: fmt.Sprintf("Actions.Results:%d:%d", runID, jobID),
|
||||
TaskID: taskID,
|
||||
RunID: runID,
|
||||
JobID: jobID,
|
||||
Ac: string(ac),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString([]byte{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||
h := req.Header.Get("Authorization")
|
||||
if h == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(h, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Errorf("split token failed: %s", h)
|
||||
return 0, fmt.Errorf("split token failed")
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return []byte{}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c, ok := token.Claims.(*actionsClaims)
|
||||
if !token.Valid || !ok {
|
||||
return 0, fmt.Errorf("invalid token claim")
|
||||
}
|
||||
|
||||
return c.TaskID, nil
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateAuthorizationToken(t *testing.T) {
|
||||
var taskID int64 = 23
|
||||
token, err := CreateAuthorizationToken(taskID, 1, 2)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, "", token)
|
||||
claims := jwt.MapClaims{}
|
||||
_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte{}, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
scp, ok := claims["scp"]
|
||||
assert.True(t, ok, "Has scp claim in jwt token")
|
||||
assert.Contains(t, scp, "Actions.Results:1:2")
|
||||
taskIDClaim, ok := claims["TaskID"]
|
||||
assert.True(t, ok, "Has TaskID claim in jwt token")
|
||||
assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one")
|
||||
acClaim, ok := claims["ac"]
|
||||
assert.True(t, ok, "Has ac claim in jwt token")
|
||||
ac, ok := acClaim.(string)
|
||||
assert.True(t, ok, "ac claim is a string for buildx gha cache")
|
||||
scopes := []actionsCacheScope{}
|
||||
err = json.Unmarshal([]byte(ac), &scopes)
|
||||
assert.NoError(t, err, "ac claim is a json list for buildx gha cache")
|
||||
assert.GreaterOrEqual(t, len(scopes), 1, "Expected at least one action cache scope for buildx gha cache")
|
||||
}
|
||||
|
||||
func TestParseAuthorizationToken(t *testing.T) {
|
||||
var taskID int64 = 23
|
||||
token, err := CreateAuthorizationToken(taskID, 1, 2)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, "", token)
|
||||
headers := http.Header{}
|
||||
headers.Set("Authorization", "Bearer "+token)
|
||||
rTaskID, err := ParseAuthorizationToken(&http.Request{
|
||||
Header: headers,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, taskID, rTaskID)
|
||||
}
|
||||
|
||||
func TestParseAuthorizationTokenNoAuthHeader(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
rTaskID, err := ParseAuthorizationToken(&http.Request{
|
||||
Header: headers,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), rTaskID)
|
||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/go-connections/nat"
|
||||
|
@ -961,10 +962,15 @@ func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
|||
actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", rc.Config.ArtifactServerAddr, rc.Config.ArtifactServerPort)
|
||||
}
|
||||
env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL
|
||||
env["ACTIONS_RESULTS_URL"] = actionsRuntimeURL
|
||||
|
||||
actionsRuntimeToken := os.Getenv("ACTIONS_RUNTIME_TOKEN")
|
||||
if actionsRuntimeToken == "" {
|
||||
actionsRuntimeToken = "token"
|
||||
runID := int64(1)
|
||||
if rid, ok := rc.Config.Env["GITHUB_RUN_ID"]; ok {
|
||||
runID, _ = strconv.ParseInt(rid, 10, 64)
|
||||
}
|
||||
actionsRuntimeToken, _ = common.CreateAuthorizationToken(runID, runID, runID)
|
||||
}
|
||||
env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue