Skip to content

Commit a18c8dd

Browse files
committed
fix private repository artifact download redirect
1 parent 44bde87 commit a18c8dd

File tree

3 files changed

+50
-79
lines changed

3 files changed

+50
-79
lines changed

routers/api/v1/api.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1248,7 +1248,6 @@ func Routes() *web.Router {
12481248
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
12491249
})
12501250
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
1251-
m.Get("/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw)
12521251
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
12531252
m.Group("/keys", func() {
12541253
m.Combo("").Get(repo.ListDeployKeys).
@@ -1409,6 +1408,9 @@ func Routes() *web.Router {
14091408
}, repoAssignment(), checkTokenPublicOnly())
14101409
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
14111410

1411+
// Artifacts direct download endpoint authenticates via signed url
1412+
m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw)
1413+
14121414
// Notifications (requires notifications scope)
14131415
m.Group("/repos", func() {
14141416
m.Group("/{username}/{reponame}", func() {

routers/api/v1/repo/action.go

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44
package repo
55

66
import (
7+
"crypto/hmac"
8+
"crypto/sha256"
9+
"encoding/base64"
710
"errors"
811
"fmt"
912
"net/http"
1013
"net/url"
1114
"strings"
15+
"time"
16+
17+
go_context "context"
1218

1319
actions_model "code.gitea.io/gitea/models/actions"
1420
"code.gitea.io/gitea/models/db"
1521
secret_model "code.gitea.io/gitea/models/secret"
1622
"code.gitea.io/gitea/modules/actions"
1723
"code.gitea.io/gitea/modules/httplib"
24+
"code.gitea.io/gitea/modules/log"
1825
"code.gitea.io/gitea/modules/setting"
1926
api "code.gitea.io/gitea/modules/structs"
2027
"code.gitea.io/gitea/modules/util"
@@ -1084,6 +1091,21 @@ func DeleteArtifact(ctx *context.APIContext) {
10841091
ctx.Error(http.StatusNotFound, "artifact not found", fmt.Errorf("artifact not found"))
10851092
}
10861093

1094+
func buildSignature(endp, expires string, artifactID int64) []byte {
1095+
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
1096+
mac.Write([]byte(endp))
1097+
mac.Write([]byte(expires))
1098+
mac.Write([]byte(fmt.Sprint(artifactID)))
1099+
return mac.Sum(nil)
1100+
}
1101+
1102+
func buildSigURL(ctx go_context.Context, endp string, artifactID int64) string {
1103+
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
1104+
uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") +
1105+
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endp, expires, artifactID)) + "&expires=" + url.QueryEscape(expires)
1106+
return uploadURL
1107+
}
1108+
10871109
// DownloadArtifact Downloads a specific artifact for a workflow run redirects to blob url.
10881110
func DownloadArtifact(ctx *context.APIContext) {
10891111
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip repository downloadArtifact
@@ -1136,9 +1158,9 @@ func DownloadArtifact(ctx *context.APIContext) {
11361158
ctx.Error(http.StatusInternalServerError, err.Error(), err)
11371159
return
11381160
}
1139-
repoName := ctx.Repo.Repository.FullName()
1140-
url := httplib.MakeAbsoluteURL(ctx, setting.AppSubURL+"/api/v1/repos/"+repoName+"/actions/artifacts/"+fmt.Sprintf("%d", art.ID)+"/zip/raw")
1141-
ctx.Redirect(url, http.StatusFound)
1161+
1162+
rurl := buildSigURL(ctx, "api/v1/repos/"+url.PathEscape(ctx.Repo.Repository.OwnerName)+"/"+url.PathEscape(ctx.Repo.Repository.Name)+"/actions/artifacts/"+fmt.Sprintf("%d", art.ID)+"/zip/raw", art.ID)
1163+
ctx.Redirect(rurl, http.StatusFound)
11421164
return
11431165
}
11441166
// v3 not supported due to not having one unique id
@@ -1147,34 +1169,26 @@ func DownloadArtifact(ctx *context.APIContext) {
11471169

11481170
// DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
11491171
func DownloadArtifactRaw(ctx *context.APIContext) {
1150-
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip/raw repository downloadArtifactRaw
1151-
// ---
1152-
// summary: Downloads a specific artifact for a workflow run directly
1153-
// produces:
1154-
// - application/json
1155-
// parameters:
1156-
// - name: owner
1157-
// in: path
1158-
// description: name of the owner
1159-
// type: string
1160-
// required: true
1161-
// - name: repo
1162-
// in: path
1163-
// description: name of the repository
1164-
// type: string
1165-
// required: true
1166-
// - name: artifact_id
1167-
// in: path
1168-
// description: id of the artifact
1169-
// type: string
1170-
// required: true
1171-
// responses:
1172-
// "200":
1173-
// description: the artifact content
1174-
// "400":
1175-
// "$ref": "#/responses/error"
1176-
// "404":
1177-
// "$ref": "#/responses/notFound"
1172+
username := ctx.PathParam("username")
1173+
reponame := ctx.PathParam("reponame")
1174+
artifactID := ctx.PathParamInt64("artifact_id")
1175+
sig := ctx.Req.URL.Query().Get("sig")
1176+
expires := ctx.Req.URL.Query().Get("expires")
1177+
dsig, _ := base64.URLEncoding.DecodeString(sig)
1178+
1179+
endp := "api/v1/repos/" + url.PathEscape(username) + "/" + url.PathEscape(reponame) + "/actions/artifacts/" + fmt.Sprintf("%d", artifactID) + "/zip/raw"
1180+
expecedsig := buildSignature(endp, expires, artifactID)
1181+
if !hmac.Equal(dsig, expecedsig) {
1182+
log.Error("Error unauthorized")
1183+
ctx.Error(http.StatusUnauthorized, "Error unauthorized", nil)
1184+
return
1185+
}
1186+
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
1187+
if err != nil || t.Before(time.Now()) {
1188+
log.Error("Error link expired")
1189+
ctx.Error(http.StatusUnauthorized, "Error link expired", nil)
1190+
return
1191+
}
11781192

11791193
art, ok := getArtifactByID(ctx)
11801194
if !ok {
@@ -1210,7 +1224,8 @@ func getArtifactByID(ctx *context.APIContext) (*actions_model.ActionArtifact, bo
12101224
return nil, false
12111225
}
12121226
// if artifacts status is not uploaded-confirmed, treat it as not found
1213-
if !ok || art.RepoID != ctx.Repo.Repository.ID || art.OwnerID != ctx.Repo.Repository.OwnerID || art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) && art.Status != int64(actions_model.ArtifactStatusExpired) {
1227+
// ctx.Repo.Repository is nil for the raw download endpoint that checked this already
1228+
if !ok || ctx.Repo != nil && ctx.Repo.Repository != nil && (art.RepoID != ctx.Repo.Repository.ID || art.OwnerID != ctx.Repo.Repository.OwnerID) || art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) && art.Status != int64(actions_model.ArtifactStatusExpired) {
12141229
ctx.Error(http.StatusNotFound, "artifact not found", fmt.Errorf("artifact not found"))
12151230
return nil, false
12161231
}

templates/swagger/v1_json.tmpl

Lines changed: 0 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)