Skip to content

Commit 3174bee

Browse files
committed
Add artifacts v4 jwt to job message and accept it
1 parent 1df06e3 commit 3174bee

File tree

3 files changed

+119
-14
lines changed

3 files changed

+119
-14
lines changed

routers/api/actions/artifacts.go

+36-14
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import (
7878
"code.gitea.io/gitea/modules/util"
7979
"code.gitea.io/gitea/modules/web"
8080
web_types "code.gitea.io/gitea/modules/web/types"
81+
actions_service "code.gitea.io/gitea/services/actions"
8182
)
8283

8384
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
@@ -129,20 +130,41 @@ func ArtifactContexter() func(next http.Handler) http.Handler {
129130
ctx := &ArtifactContext{Base: base}
130131
ctx.AppendContextValue(artifactContextKey, ctx)
131132

132-
// action task call server api with Bearer ACTIONS_RUNTIME_TOKEN
133-
// we should verify the ACTIONS_RUNTIME_TOKEN
134-
authHeader := req.Header.Get("Authorization")
135-
if len(authHeader) == 0 || !strings.HasPrefix(authHeader, "Bearer ") {
136-
ctx.Error(http.StatusUnauthorized, "Bad authorization header")
137-
return
138-
}
139-
140-
authToken := strings.TrimPrefix(authHeader, "Bearer ")
141-
task, err := actions.GetRunningTaskByToken(req.Context(), authToken)
142-
if err != nil {
143-
log.Error("Error runner api getting task: %v", err)
144-
ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
145-
return
133+
// New act_runner uses jwt to authenticate
134+
tID, err := actions_service.ParseAuthorizationToken(req)
135+
136+
var task *actions.ActionTask
137+
if err == nil {
138+
139+
task, err = actions.GetTaskByID(req.Context(), tID)
140+
if err != nil {
141+
log.Error("Error runner api getting task by ID: %v", err)
142+
ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
143+
return
144+
}
145+
if task.Status != actions.StatusRunning {
146+
log.Error("Error runner api getting task: task is not running")
147+
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
148+
return
149+
}
150+
} else {
151+
// Old act_runner uses GITEA_TOKEN to authenticate
152+
// action task call server api with Bearer ACTIONS_RUNTIME_TOKEN
153+
// we should verify the ACTIONS_RUNTIME_TOKEN
154+
authHeader := req.Header.Get("Authorization")
155+
if len(authHeader) == 0 || !strings.HasPrefix(authHeader, "Bearer ") {
156+
ctx.Error(http.StatusUnauthorized, "Bad authorization header")
157+
return
158+
}
159+
160+
authToken := strings.TrimPrefix(authHeader, "Bearer ")
161+
162+
task, err = actions.GetRunningTaskByToken(req.Context(), authToken)
163+
if err != nil {
164+
log.Error("Error runner api getting task: %v", err)
165+
ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
166+
return
167+
}
146168
}
147169

148170
if err := task.LoadJob(req.Context()); err != nil {

routers/api/actions/runner/utils.go

+6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
151151

152152
refName := git.RefName(ref)
153153

154+
giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
155+
if err != nil {
156+
log.Error("actions.CreateAuthorizationToken failed: %v", err)
157+
}
158+
154159
taskContext, err := structpb.NewStruct(map[string]any{
155160
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
156161
"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.
@@ -190,6 +195,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
190195

191196
// additional contexts
192197
"gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(),
198+
"gitea_runtime_token": giteaRuntimeToken,
193199
})
194200
if err != nil {
195201
log.Error("structpb.NewStruct failed: %v", err)

services/actions/auth.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"code.gitea.io/gitea/modules/log"
13+
"code.gitea.io/gitea/modules/setting"
14+
15+
"github.com/golang-jwt/jwt/v5"
16+
)
17+
18+
type actionsClaims struct {
19+
jwt.RegisteredClaims
20+
Scp string `json:"scp"`
21+
TaskID int64
22+
RunID int64
23+
JobID int64
24+
}
25+
26+
func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
27+
now := time.Now()
28+
29+
claims := actionsClaims{
30+
RegisteredClaims: jwt.RegisteredClaims{
31+
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
32+
NotBefore: jwt.NewNumericDate(now),
33+
},
34+
Scp: fmt.Sprintf("Actions.Results:%d:%d", runID, jobID),
35+
TaskID: taskID,
36+
RunID: runID,
37+
JobID: jobID,
38+
}
39+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
40+
41+
tokenString, err := token.SignedString([]byte(setting.SecretKey))
42+
if err != nil {
43+
return "", err
44+
}
45+
46+
return tokenString, nil
47+
}
48+
49+
func ParseAuthorizationToken(req *http.Request) (int64, error) {
50+
h := req.Header.Get("Authorization")
51+
if h == "" {
52+
return 0, nil
53+
}
54+
55+
parts := strings.SplitN(h, " ", 2)
56+
if len(parts) != 2 {
57+
log.Error("split token failed: %s", h)
58+
return 0, fmt.Errorf("split token failed")
59+
}
60+
61+
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
62+
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
63+
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
64+
}
65+
return []byte(setting.SecretKey), nil
66+
})
67+
if err != nil {
68+
return 0, err
69+
}
70+
71+
c, ok := token.Claims.(*actionsClaims)
72+
if !token.Valid || !ok {
73+
return 0, fmt.Errorf("invalid token claim")
74+
}
75+
76+
return c.TaskID, nil
77+
}

0 commit comments

Comments
 (0)