Merge pull request '[gitea] week 2024-48-v9.0 cherry pick (gitea/main -> v9.0/forgejo)' (#6064) from earl-warren/wcp/2024-48-v9.0 into v9.0/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6064 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
a494510972
10 changed files with 186 additions and 34 deletions
|
@ -1,3 +1,22 @@
|
||||||
|
-
|
||||||
|
id: 46
|
||||||
|
attempt: 3
|
||||||
|
runner_id: 1
|
||||||
|
status: 3 # 3 is the status code for "cancelled"
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa
|
||||||
|
token_salt: eeeeeeee
|
||||||
|
token_last_eight: eeeeeeee
|
||||||
|
log_filename: artifact-test2/2f/47.log
|
||||||
|
log_in_storage: 1
|
||||||
|
log_length: 707
|
||||||
|
log_size: 90179
|
||||||
|
log_expired: 0
|
||||||
-
|
-
|
||||||
id: 47
|
id: 47
|
||||||
job_id: 192
|
job_id: 192
|
||||||
|
|
|
@ -332,6 +332,7 @@
|
||||||
repo_admin_change_team_access: false
|
repo_admin_change_team_access: false
|
||||||
theme: ""
|
theme: ""
|
||||||
keep_activity_private: false
|
keep_activity_private: false
|
||||||
|
created_unix: 1730468968
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 10
|
id: 10
|
||||||
|
|
|
@ -50,19 +50,19 @@ const (
|
||||||
UserTypeIndividual UserType = iota // Historic reason to make it starts at 0.
|
UserTypeIndividual UserType = iota // Historic reason to make it starts at 0.
|
||||||
|
|
||||||
// UserTypeOrganization defines an organization
|
// UserTypeOrganization defines an organization
|
||||||
UserTypeOrganization
|
UserTypeOrganization // 1
|
||||||
|
|
||||||
// UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on
|
// UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on
|
||||||
UserTypeUserReserved
|
UserTypeUserReserved // 2
|
||||||
|
|
||||||
// UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved
|
// UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved
|
||||||
UserTypeOrganizationReserved
|
UserTypeOrganizationReserved // 3
|
||||||
|
|
||||||
// UserTypeBot defines a bot user
|
// UserTypeBot defines a bot user
|
||||||
UserTypeBot
|
UserTypeBot // 4
|
||||||
|
|
||||||
// UserTypeRemoteUser defines a remote user for federated users
|
// UserTypeRemoteUser defines a remote user for federated users
|
||||||
UserTypeRemoteUser
|
UserTypeRemoteUser // 5
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -917,7 +917,13 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
|
||||||
|
|
||||||
// GetInactiveUsers gets all inactive users
|
// GetInactiveUsers gets all inactive users
|
||||||
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
|
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
|
||||||
var cond builder.Cond = builder.Eq{"is_active": false}
|
cond := builder.And(
|
||||||
|
builder.Eq{"is_active": false},
|
||||||
|
builder.Or( // only plain user
|
||||||
|
builder.Eq{"`type`": UserTypeIndividual},
|
||||||
|
builder.Eq{"`type`": UserTypeUserReserved},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if olderThan > 0 {
|
if olderThan > 0 {
|
||||||
cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()})
|
cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()})
|
||||||
|
|
|
@ -765,3 +765,17 @@ func TestVerifyUserAuthorizationToken(t *testing.T) {
|
||||||
assert.Nil(t, authToken)
|
assert.Nil(t, authToken)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetInactiveUsers(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// all inactive users
|
||||||
|
// user1's createdunix is 1730468968
|
||||||
|
users, err := user_model.GetInactiveUsers(db.DefaultContext, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, users, 1)
|
||||||
|
interval := time.Now().Unix() - 1730468968 + 3600*24
|
||||||
|
users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, users)
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Commit represents a git commit.
|
// Commit represents a git commit.
|
||||||
|
@ -365,37 +367,32 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rd, err := entry.Blob().DataAsync()
|
content, err := entry.Blob().GetBlobContent(10 * 1024)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rd.Close()
|
c.submoduleCache, err = parseSubmoduleContent([]byte(content))
|
||||||
scanner := bufio.NewScanner(rd)
|
if err != nil {
|
||||||
c.submoduleCache = newObjectCache()
|
return nil, err
|
||||||
var ismodule bool
|
|
||||||
var path string
|
|
||||||
for scanner.Scan() {
|
|
||||||
if strings.HasPrefix(scanner.Text(), "[submodule") {
|
|
||||||
ismodule = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ismodule {
|
|
||||||
fields := strings.Split(scanner.Text(), "=")
|
|
||||||
k := strings.TrimSpace(fields[0])
|
|
||||||
if k == "path" {
|
|
||||||
path = strings.TrimSpace(fields[1])
|
|
||||||
} else if k == "url" {
|
|
||||||
c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])})
|
|
||||||
ismodule = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err = scanner.Err(); err != nil {
|
return c.submoduleCache, nil
|
||||||
return nil, fmt.Errorf("GetSubModules scan: %w", err)
|
}
|
||||||
|
|
||||||
|
func parseSubmoduleContent(bs []byte) (*ObjectCache, error) {
|
||||||
|
cfg := config.NewModules()
|
||||||
|
if err := cfg.Unmarshal(bs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
submoduleCache := newObjectCache()
|
||||||
|
if len(cfg.Submodules) == 0 {
|
||||||
|
return nil, fmt.Errorf("no submodules found")
|
||||||
|
}
|
||||||
|
for _, subModule := range cfg.Submodules {
|
||||||
|
submoduleCache.Set(subModule.Path, subModule.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.submoduleCache, nil
|
return submoduleCache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubModule get the sub module according entryname
|
// GetSubModule get the sub module according entryname
|
||||||
|
|
|
@ -369,3 +369,33 @@ func TestParseCommitRenames(t *testing.T) {
|
||||||
assert.Equal(t, testcase.renames, renames)
|
assert.Equal(t, testcase.renames, renames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_parseSubmoduleContent(t *testing.T) {
|
||||||
|
submoduleFiles := []struct {
|
||||||
|
fileContent string
|
||||||
|
expectedPath string
|
||||||
|
expectedURL string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fileContent: `[submodule "jakarta-servlet"]
|
||||||
|
url = ../../ALP-pool/jakarta-servlet
|
||||||
|
path = jakarta-servlet`,
|
||||||
|
expectedPath: "jakarta-servlet",
|
||||||
|
expectedURL: "../../ALP-pool/jakarta-servlet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileContent: `[submodule "jakarta-servlet"]
|
||||||
|
path = jakarta-servlet
|
||||||
|
url = ../../ALP-pool/jakarta-servlet`,
|
||||||
|
expectedPath: "jakarta-servlet",
|
||||||
|
expectedURL: "../../ALP-pool/jakarta-servlet",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, kase := range submoduleFiles {
|
||||||
|
submodule, err := parseSubmoduleContent([]byte(kase.fileContent))
|
||||||
|
require.NoError(t, err)
|
||||||
|
v, ok := submodule.Get(kase.expectedPath)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, kase.expectedURL, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
3
release-notes/6064.md
Normal file
3
release-notes/6064.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/bf520f5184327eea9590ac5eb52d98f16af43c12) Fix GetInactiveUsers
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/1c04f8f10a7ebc02f41af3d2db6a2a4e85127441) Fix submodule parsing
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/48872d11ca920849ec174f76c0d667ca2b289aef) allow the actions user to login via the jwt token
|
|
@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||||
return 0, fmt.Errorf("split token failed")
|
return 0, fmt.Errorf("split token failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
return TokenToTaskID(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenToTaskID returns the TaskID associated with the provided JWT token
|
||||||
|
func TokenToTaskID(token string) (int64, error) {
|
||||||
|
parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
}
|
}
|
||||||
|
@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, ok := token.Claims.(*actionsClaims)
|
c, ok := parsedToken.Claims.(*actionsClaims)
|
||||||
if !token.Valid || !ok {
|
if !parsedToken.Valid || !ok {
|
||||||
return 0, fmt.Errorf("invalid token claim")
|
return 0, fmt.Errorf("invalid token claim")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
"code.gitea.io/gitea/services/actions"
|
||||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,6 +95,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) (int64, stri
|
||||||
return grant.UserID, grantScopes
|
return grant.UserID, grantScopes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
|
||||||
|
func CheckTaskIsRunning(ctx context.Context, taskID int64) bool {
|
||||||
|
// Verify the task exists
|
||||||
|
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that it's running
|
||||||
|
return task.Status == actions_model.StatusRunning
|
||||||
|
}
|
||||||
|
|
||||||
// OAuth2 implements the Auth interface and authenticates requests
|
// OAuth2 implements the Auth interface and authenticates requests
|
||||||
// (API requests only) by looking for an OAuth token in query parameters or the
|
// (API requests only) by looking for an OAuth token in query parameters or the
|
||||||
// "Authorization" header.
|
// "Authorization" header.
|
||||||
|
@ -137,8 +150,17 @@ func parseToken(req *http.Request) (string, bool) {
|
||||||
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
||||||
// Let's see if token is valid.
|
// Let's see if token is valid.
|
||||||
if strings.Contains(tokenSHA, ".") {
|
if strings.Contains(tokenSHA, ".") {
|
||||||
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
|
// First attempt to decode an actions JWT, returning the actions user
|
||||||
|
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
|
||||||
|
if CheckTaskIsRunning(ctx, taskID) {
|
||||||
|
store.GetData()["IsActionsToken"] = true
|
||||||
|
store.GetData()["ActionsTaskID"] = taskID
|
||||||
|
return user_model.ActionsUserID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check if this is an OAuth access token
|
||||||
|
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
|
||||||
if uid != 0 {
|
if uid != 0 {
|
||||||
store.GetData()["IsApiToken"] = true
|
store.GetData()["IsApiToken"] = true
|
||||||
if grantScopes != "" {
|
if grantScopes != "" {
|
||||||
|
|
55
services/auth/oauth2_test.go
Normal file
55
services/auth/oauth2_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
"code.gitea.io/gitea/services/actions"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserIDFromToken(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
t.Run("Actions JWT", func(t *testing.T) {
|
||||||
|
const RunningTaskID = 47
|
||||||
|
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ds := make(middleware.ContextData)
|
||||||
|
|
||||||
|
o := OAuth2{}
|
||||||
|
uid := o.userIDFromToken(context.Background(), token, ds)
|
||||||
|
assert.Equal(t, int64(user_model.ActionsUserID), uid)
|
||||||
|
assert.Equal(t, true, ds["IsActionsToken"])
|
||||||
|
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckTaskIsRunning(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
cases := map[string]struct {
|
||||||
|
TaskID int64
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
"Running": {TaskID: 47, Expected: true},
|
||||||
|
"Missing": {TaskID: 1, Expected: false},
|
||||||
|
"Cancelled": {TaskID: 46, Expected: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range cases {
|
||||||
|
c := cases[name]
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
actual := CheckTaskIsRunning(context.Background(), c.TaskID)
|
||||||
|
assert.Equal(t, c.Expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue