// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( "bytes" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/xml" "fmt" "io" "mime" "net/http" "strings" "testing" "time" actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/actions" actions_service "code.gitea.io/gitea/services/actions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) func toProtoJSON(m protoreflect.ProtoMessage) io.Reader { resp, _ := protojson.Marshal(m) buf := bytes.Buffer{} buf.Write(resp) return &buf } func TestActionsArtifactV4UploadSingleFile(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) table := []struct { name string version int32 contentType string blockID bool noLength bool append int path string }{ { name: "artifact", version: 4, path: "artifact.zip", }, { name: "artifact2", version: 4, blockID: true, }, { name: "artifact3", version: 4, noLength: true, }, { name: "artifact4", version: 4, blockID: true, noLength: true, }, { name: "artifact5", version: 7, blockID: true, }, { name: "artifact6", version: 7, append: 2, noLength: true, }, { name: "artifact7", version: 7, append: 3, blockID: true, noLength: true, }, { name: "artifact8", version: 7, append: 4, blockID: true, }, { name: "artifact9.json", version: 7, contentType: "application/json", }, { name: "artifact10", version: 7, contentType: "application/zip", path: "artifact10.zip", }, { name: "artifact11.zip", version: 7, contentType: "application/zip", path: "artifact11.zip", }, } for _, entry := range table { t.Run(entry.name, func(t *testing.T) { // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ Version: entry.version, Name: entry.name, WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil), })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) assert.True(t, uploadResp.Ok) assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") h := sha256.New() blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0)) // get upload url for i := range entry.append + 1 { url := uploadResp.SignedUploadUrl // See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block // See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block if entry.blockID { blockID := base64.RawURLEncoding.EncodeToString(fmt.Append([]byte("SOME_BIG_BLOCK_ID_"), i)) blocks = append(blocks, blockID) url += "&comp=block&blockid=" + blockID } else { url += "&comp=appendBlock" } // upload artifact chunk body := strings.Repeat("A", 1024) _, _ = h.Write([]byte(body)) var bodyReader io.Reader = strings.NewReader(body) if entry.noLength { bodyReader = io.MultiReader(bodyReader) } req = NewRequestWithBody(t, "PUT", url, bodyReader) MakeRequest(t, req, http.StatusCreated) } if entry.blockID && entry.append > 0 { // https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact blockList blockList := &actions.BlockList{ Latest: blocks, } rawBlockList, err := xml.Marshal(blockList) assert.NoError(t, err) req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) MakeRequest(t, req, http.StatusCreated) } sha := h.Sum(nil) t.Logf("Create artifact confirm") // confirm artifact upload req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ Name: entry.name, Size: int64(entry.append+1) * 1024, Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha)), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var finalizeResp actions.FinalizeArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) assert.True(t, finalizeResp.Ok) artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId}) if entry.contentType != "" { assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType) } else { assert.Equal(t, "application/zip", artifact.ContentEncodingOrType) } if entry.path != "" { assert.Equal(t, entry.path, artifact.ArtifactPath) } assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status) assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize) assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize) }) } } func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ Version: 4, Name: "artifact-invalid-checksum", WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) assert.True(t, uploadResp.Ok) assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload url url := uploadResp.SignedUploadUrl + "&comp=block" // upload artifact chunk body := strings.Repeat("B", 1024) req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) MakeRequest(t, req, http.StatusCreated) t.Logf("Create artifact confirm") sha := sha256.Sum256([]byte(strings.Repeat("A", 1024))) // confirm artifact upload req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ Name: "artifact-invalid-checksum", Size: 1024, Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) MakeRequest(t, req, http.StatusInternalServerError) } func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ Version: 4, ExpiresAt: timestamppb.New(time.Now().Add(5 * 24 * time.Hour)), Name: "artifactWithRetentionDays", WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) assert.True(t, uploadResp.Ok) assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload url url := uploadResp.SignedUploadUrl + "&comp=block" // upload artifact chunk body := strings.Repeat("A", 1024) req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) MakeRequest(t, req, http.StatusCreated) t.Logf("Create artifact confirm") sha := sha256.Sum256([]byte(body)) // confirm artifact upload req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ Name: "artifactWithRetentionDays", Size: 1024, Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var finalizeResp actions.FinalizeArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) assert.True(t, finalizeResp.Ok) } func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ Version: 4, Name: "artifactWithPotentialHarmfulBlockID", WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) assert.True(t, uploadResp.Ok) assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload urls url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile" blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact chunk body := strings.Repeat("A", 1024) req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) MakeRequest(t, req, http.StatusCreated) // verify that the exploit didn't work _, err = storage.Actions.Stat("myfile") assert.Error(t, err) // upload artifact blockList blockList := &actions.BlockList{ Latest: []string{ "/../myfile", }, } rawBlockList, err := xml.Marshal(blockList) assert.NoError(t, err) req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) MakeRequest(t, req, http.StatusCreated) t.Logf("Create artifact confirm") sha := sha256.Sum256([]byte(body)) // confirm artifact upload req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ Name: "artifactWithPotentialHarmfulBlockID", Size: 1024, Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var finalizeResp actions.FinalizeArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) assert.True(t, finalizeResp.Ok) } func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) table := []struct { name string artifactName string serveDirect bool contentType string }{ {name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""}, {name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"}, {name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"}, {name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true}, {name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true}, {name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true}, } for _, entry := range table { t.Run(entry.name, func(t *testing.T) { // Only AzureBlobStorageType supports ServeDirect Uploads switch setting.Actions.ArtifactStorage.Type { case setting.AzureBlobStorageType: defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)() default: if entry.serveDirect { t.Skip() } } // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ Version: util.Iif[int32](entry.contentType != "", 7, 4), Name: entry.artifactName, WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil), })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) assert.True(t, uploadResp.Ok) if !entry.serveDirect { assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") } // get upload urls block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1")) block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2")) blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact chunks bodyb := strings.Repeat("B", 1024) req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) if entry.serveDirect { req.Request.RequestURI = "" nresp, err := http.DefaultClient.Do(req.Request) require.NoError(t, err) nresp.Body.Close() require.Equal(t, http.StatusCreated, nresp.StatusCode) } else { MakeRequest(t, req, http.StatusCreated) } bodya := strings.Repeat("A", 1024) req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) if entry.serveDirect { req.Request.RequestURI = "" nresp, err := http.DefaultClient.Do(req.Request) require.NoError(t, err) nresp.Body.Close() require.Equal(t, http.StatusCreated, nresp.StatusCode) } else { MakeRequest(t, req, http.StatusCreated) } // upload artifact blockList blockList := &actions.BlockList{ Latest: []string{ base64.RawURLEncoding.EncodeToString([]byte("block1")), base64.RawURLEncoding.EncodeToString([]byte("block2")), }, } rawBlockList, err := xml.Marshal(blockList) assert.NoError(t, err) req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) if entry.serveDirect { req.Request.RequestURI = "" nresp, err := http.DefaultClient.Do(req.Request) require.NoError(t, err) nresp.Body.Close() require.Equal(t, http.StatusCreated, nresp.StatusCode) } else { MakeRequest(t, req, http.StatusCreated) } t.Logf("Create artifact confirm") sha := sha256.Sum256([]byte(bodya + bodyb)) // confirm artifact upload req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ Name: entry.artifactName, Size: 2048, Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var finalizeResp actions.FinalizeArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) assert.True(t, finalizeResp.Ok) artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId}) if entry.contentType != "" { assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType) } else { assert.Equal(t, "application/zip", artifact.ContentEncodingOrType) } assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status) assert.Equal(t, int64(2048), artifact.FileSize) assert.Equal(t, int64(2048), artifact.FileCompressedSize) }) } } func TestActionsArtifactV4DownloadSingle(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) table := []struct { Name string ArtifactName string FileName string ServeDirect bool ContentType string ContentDisposition string }{ {Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"}, {Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"}, {Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"}, {Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true}, {Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true}, {Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true}, } for _, entry := range table { t.Run(entry.Name, func(t *testing.T) { switch setting.Actions.ArtifactStorage.Type { case setting.AzureBlobStorageType: defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)() case setting.MinioStorageType: defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)() default: if entry.ServeDirect { t.Skip() } } // list artifacts by name req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ NameFilter: wrapperspb.String(entry.ArtifactName), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var listResp actions.ListArtifactsResponse require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp)) require.Len(t, listResp.Artifacts, 1) // list artifacts by id req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp)) assert.Len(t, listResp.Artifacts, 1) // acquire artifact download url req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ Name: entry.ArtifactName, WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var finalizeResp actions.GetSignedArtifactURLResponse require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)) assert.NotEmpty(t, finalizeResp.SignedUrl) body := strings.Repeat("D", 1024) var contentDisposition string if entry.ServeDirect { externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil) require.NoError(t, err) externalResp, err := http.DefaultClient.Do(externalReq) require.NoError(t, err) assert.Equal(t, http.StatusOK, externalResp.StatusCode) assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type")) contentDisposition = externalResp.Header.Get("Content-Disposition") buf := make([]byte, 1024) n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf)) externalResp.Body.Close() require.NoError(t, err) assert.Equal(t, len(buf), n) assert.Equal(t, body, string(buf)) } else { req = NewRequest(t, "GET", finalizeResp.SignedUrl) resp = MakeRequest(t, req, http.StatusOK) assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type")) contentDisposition = resp.Header().Get("Content-Disposition") assert.Equal(t, body, resp.Body.String()) } disposition, param, err := mime.ParseMediaType(contentDisposition) require.NoError(t, err) assert.Equal(t, "inline", disposition) assert.Equal(t, entry.FileName, param["filename"]) }) } } func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifact can be listed and found by name req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/792/artifacts?name=artifact-v4-download", repo.FullName()), nil). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var listResp api.ActionArtifactsResponse err := json.Unmarshal(resp.Body.Bytes(), &listResp) assert.NoError(t, err) assert.NotEmpty(t, listResp.Entries[0].ArchiveDownloadURL) assert.Equal(t, "artifact-v4-download", listResp.Entries[0].Name) // confirm artifact blob storage url can be retrieved req = NewRequestWithBody(t, "GET", listResp.Entries[0].ArchiveDownloadURL, nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusFound) // confirm artifact can be downloaded and has expected content req = NewRequestWithBody(t, "GET", resp.Header().Get("Location"), nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) body := strings.Repeat("D", 1024) assert.Equal(t, body, resp.Body.String()) } func TestActionsArtifactV4DownloadSinglePublicApi(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifact can be listed and found by name req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts?name=artifact-v4-download", repo.FullName()), nil). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var listResp api.ActionArtifactsResponse err := json.Unmarshal(resp.Body.Bytes(), &listResp) assert.NoError(t, err) assert.NotEmpty(t, listResp.Entries[0].ArchiveDownloadURL) assert.Equal(t, "artifact-v4-download", listResp.Entries[0].Name) // confirm artifact blob storage url can be retrieved req = NewRequestWithBody(t, "GET", listResp.Entries[0].ArchiveDownloadURL, nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusFound) blobLocation := resp.Header().Get("Location") // confirm artifact can be downloaded without token and has expected content req = NewRequestWithBody(t, "GET", blobLocation, nil) resp = MakeRequest(t, req, http.StatusOK) body := strings.Repeat("D", 1024) assert.Equal(t, body, resp.Body.String()) // confirm artifact can not be downloaded without query req = NewRequestWithBody(t, "GET", blobLocation, nil) req.URL.RawQuery = "" _ = MakeRequest(t, req, http.StatusUnauthorized) } func TestActionsArtifactV4DownloadSinglePublicApiPrivateRepo(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifact can be listed and found by name req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts?name=artifact-v4-download", repo.FullName()), nil). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var listResp api.ActionArtifactsResponse err := json.Unmarshal(resp.Body.Bytes(), &listResp) assert.NoError(t, err) assert.Equal(t, int64(23), listResp.Entries[0].ID) assert.NotEmpty(t, listResp.Entries[0].ArchiveDownloadURL) assert.Equal(t, "artifact-v4-download", listResp.Entries[0].Name) // confirm artifact blob storage url can be retrieved req = NewRequestWithBody(t, "GET", listResp.Entries[0].ArchiveDownloadURL, nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusFound) blobLocation := resp.Header().Get("Location") // confirm artifact can be downloaded without token and has expected content req = NewRequestWithBody(t, "GET", blobLocation, nil) resp = MakeRequest(t, req, http.StatusOK) body := strings.Repeat("D", 1024) assert.Equal(t, body, resp.Body.String()) // confirm artifact can not be downloaded without query req = NewRequestWithBody(t, "GET", blobLocation, nil) req.URL.RawQuery = "" _ = MakeRequest(t, req, http.StatusUnauthorized) } func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifact can be listed req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts", repo.FullName()), nil). AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var listResp api.ActionArtifactsResponse err := json.Unmarshal(resp.Body.Bytes(), &listResp) assert.NoError(t, err) for _, artifact := range listResp.Entries { assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID)) assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID)) req = NewRequestWithBody(t, "GET", artifact.URL, nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var artifactResp api.ActionArtifact err := json.Unmarshal(resp.Body.Bytes(), &artifactResp) assert.NoError(t, err) assert.Equal(t, artifact.ID, artifactResp.ID) assert.Equal(t, artifact.Name, artifactResp.Name) assert.Equal(t, artifact.SizeInBytes, artifactResp.SizeInBytes) assert.Equal(t, artifact.URL, artifactResp.URL) assert.Equal(t, artifact.ArchiveDownloadURL, artifactResp.ArchiveDownloadURL) } } func TestActionsArtifactV4GetArtifactMismatchedRepoNotFound(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifacts of wrong repo is not visible req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) } func TestActionsArtifactV4DownloadArtifactMismatchedRepoNotFound(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifacts of wrong repo is not visible req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) } func TestActionsArtifactV4DownloadArtifactCorrectRepoFound(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifacts of correct repo is visible req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusFound) } func TestActionsArtifactV4DownloadRawArtifactCorrectRepoMissingSignatureUnauthorized(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm cannot use the raw artifact endpoint even with a correct access token req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip/raw", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusUnauthorized) } func TestActionsArtifactV4Delete(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) // delete artifact by name req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/DeleteArtifact", toProtoJSON(&actions.DeleteArtifactRequest{ Name: "artifact-v4-download", WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var deleteResp actions.DeleteArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &deleteResp) assert.True(t, deleteResp.Ok) // confirm artifact is no longer accessible by GetSignedArtifactURL req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ Name: "artifact-v4-download", WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })). AddTokenAuth(token) _ = MakeRequest(t, req, http.StatusNotFound) // confirm artifact is no longer enumerateable by ListArtifacts and returns length == 0 without error req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ NameFilter: wrapperspb.String("artifact-v4-download"), WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", })).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var listResp actions.ListArtifactsResponse protojson.Unmarshal(resp.Body.Bytes(), &listResp) assert.Empty(t, listResp.Artifacts) } func TestActionsArtifactV4DeletePublicApi(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) // confirm artifacts exists req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // delete artifact by id req = NewRequestWithBody(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) // confirm artifacts has been deleted req = NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) } func TestActionsArtifactV4DeletePublicApiNotAllowedReadScope(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) // confirm artifacts exists req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // try delete artifact by id req = NewRequestWithBody(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) // confirm artifacts has not been deleted req = NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) }