mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-13 09:30:57 +00:00
Co-authored-by: bircni <bircni@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
280 lines
9.9 KiB
Go
280 lines
9.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
audit_model "gitea.dev/models/audit"
|
|
repository_model "gitea.dev/models/repo"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/reqctx"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/test"
|
|
"gitea.dev/modules/web/middleware"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func BenchmarkRecordDisabled(b *testing.B) {
|
|
defer test.MockVariableValue(&setting.Audit.RecordOutput, setting.AuditRecordOutputDisabled)()
|
|
ctx := context.Background()
|
|
u := &user_model.User{ID: 1, Name: "user"}
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
RecordAs(ctx, u, audit_model.UserPassword, u)
|
|
}
|
|
}
|
|
|
|
func BenchmarkBuildEvent(b *testing.B) {
|
|
params := RecordParams{
|
|
Action: audit_model.RepositoryMirrorPushAdd,
|
|
Actor: audit_model.EntityRef{Type: audit_model.ScopeUser, ID: 1, Name: "actor"},
|
|
Scope: audit_model.EntityRef{Type: audit_model.ScopeRepository, ID: 2, Name: "owner/repo"},
|
|
Metadata: map[string]any{
|
|
"remote_address": "https://example.com/repo.git",
|
|
},
|
|
}
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = buildEvent(context.Background(), params)
|
|
}
|
|
}
|
|
|
|
// newRequestContext mimics what routers/common.AuthShared publishes for a
|
|
// signed-in request.
|
|
func newRequestContext(t *testing.T, signedIn *user_model.User) context.Context {
|
|
t.Helper()
|
|
rc := reqctx.NewRequestContextForTest(t)
|
|
rc.GetData()[middleware.ContextDataKeySignedUser] = signedIn
|
|
return rc
|
|
}
|
|
|
|
func TestBuildEvent(t *testing.T) {
|
|
doer := &user_model.User{ID: 2, Name: "Doer"}
|
|
u := &user_model.User{ID: 1, Name: "TestUser"}
|
|
|
|
t.Run("MessageFromTemplate", func(t *testing.T) {
|
|
e := buildEvent(context.Background(), RecordParams{
|
|
Action: audit_model.UserCreate,
|
|
Actor: actorRef(doer),
|
|
Scope: ScopeFromUser(u),
|
|
})
|
|
|
|
assert.Equal(t, audit_model.UserCreate, e.Action)
|
|
assert.Equal(t, audit_model.EntityRef{Type: audit_model.ScopeUser, ID: 2, Name: "Doer"}, e.Actor())
|
|
assert.Equal(t, audit_model.EntityRef{Type: audit_model.ScopeUser, ID: 1, Name: "TestUser"}, e.Scope())
|
|
assert.Equal(t, "Created user TestUser.", e.Message)
|
|
})
|
|
|
|
t.Run("MetadataFillsPlaceholders", func(t *testing.T) {
|
|
r := &repository_model.Repository{ID: 3, Name: "TestRepo", OwnerName: "TestUser"}
|
|
m := &repository_model.PushMirror{ID: 4, RemoteAddress: "git@example.com:repo.git"}
|
|
|
|
e := buildEvent(context.Background(), RecordParams{
|
|
Action: audit_model.RepositoryMirrorPushAdd,
|
|
Actor: actorRef(doer),
|
|
Scope: ScopeFromRepository(r),
|
|
Metadata: metaPairs(
|
|
"mirror_id", m.ID,
|
|
"remote_address", m.RemoteAddress,
|
|
),
|
|
})
|
|
|
|
assert.Equal(t, "TestUser/TestRepo", e.ScopeName)
|
|
assert.Equal(t, "Added push mirror to git@example.com:repo.git for repository TestUser/TestRepo.", e.Message)
|
|
assert.InDelta(t, float64(m.ID), audit_model.DecodeMetadata(e.Metadata)["mirror_id"], 0)
|
|
})
|
|
|
|
t.Run("StatusChangesIncludeTheirNewValue", func(t *testing.T) {
|
|
e := buildEvent(context.Background(), RecordParams{
|
|
Action: audit_model.UserRestricted,
|
|
Actor: actorRef(doer),
|
|
Scope: ScopeFromUser(u),
|
|
Metadata: metaPairs("restricted", true),
|
|
})
|
|
|
|
assert.Equal(t, "Changed restricted status of user TestUser to true.", e.Message)
|
|
})
|
|
|
|
t.Run("SystemActorNamesTaskOrKey", func(t *testing.T) {
|
|
actions := user_model.NewActionsUserWithTaskID(42)
|
|
e := buildEvent(context.Background(), RecordParams{
|
|
Action: audit_model.UserCreate,
|
|
Actor: actorRef(actions),
|
|
ActorCredential: actorCredential(context.Background(), actions),
|
|
Scope: ScopeFromUser(u),
|
|
})
|
|
assert.Equal(t, user_model.ActionsUserID, e.ActorID)
|
|
assert.Equal(t, "gitea-actions:42", e.ActorCredential)
|
|
|
|
key := user_model.NewDeployKeyUserWithKeyID(7)
|
|
assert.Equal(t, "deploy-key:7", actorCredential(context.Background(), key))
|
|
assert.Empty(t, actorCredential(context.Background(), doer))
|
|
})
|
|
|
|
t.Run("CredentialFromRequest", func(t *testing.T) {
|
|
ctx := newRequestContext(t, doer)
|
|
middleware.GetContextData(ctx)[middleware.ContextDataKeyAuthCredential] = "access-token:9"
|
|
assert.Equal(t, "access-token:9", actorCredential(ctx, doer))
|
|
|
|
// an event recorded for someone other than the signed-in user is not
|
|
// tied to the credential of that request
|
|
assert.Empty(t, actorCredential(ctx, u))
|
|
})
|
|
|
|
t.Run("RequestInfo", func(t *testing.T) {
|
|
params := RecordParams{Action: audit_model.UserCreate, Actor: actorRef(doer), Scope: ScopeFromUser(u)}
|
|
|
|
e := buildEvent(context.Background(), params)
|
|
assert.Empty(t, e.IPAddress)
|
|
assert.Equal(t, audit_model.OriginSystem, e.Origin)
|
|
|
|
cliCtx := WithOrigin(context.Background(), audit_model.OriginCLI)
|
|
assert.Equal(t, audit_model.OriginCLI, buildEvent(cliCtx, params).Origin)
|
|
|
|
apiCtx := reqctx.NewRequestContextForTest(t)
|
|
SetRequestInfo(apiCtx, audit_model.OriginAPI, "127.0.0.1")
|
|
e = buildEvent(apiCtx, params)
|
|
assert.Equal(t, "127.0.0.1", e.IPAddress)
|
|
assert.Equal(t, audit_model.OriginAPI, e.Origin)
|
|
|
|
// an explicit origin wins over the one of the surrounding request
|
|
systemAPIContext := WithOrigin(apiCtx, audit_model.OriginSystem)
|
|
assert.Equal(t, audit_model.OriginSystem, buildEvent(systemAPIContext, params).Origin)
|
|
})
|
|
}
|
|
|
|
func TestEntityRefDisplay(t *testing.T) {
|
|
ref := audit_model.EntityRef{Type: audit_model.ScopeUser, ID: 1, Name: "TestUser"}
|
|
assert.Equal(t, "TestUser", ref.DisplayName())
|
|
assert.Equal(t, "/TestUser", ref.HomeLink())
|
|
assert.True(t, ref.HasLink())
|
|
|
|
sys := ScopeSystem()
|
|
assert.Equal(t, "System", sys.DisplayName())
|
|
assert.Empty(t, sys.HomeLink())
|
|
assert.False(t, sys.HasLink())
|
|
|
|
// a scope whose entity was deleted keeps its ID but has no name to link to
|
|
deleted := audit_model.EntityRef{Type: audit_model.ScopeRepository, ID: 3}
|
|
assert.Empty(t, deleted.DisplayName())
|
|
assert.False(t, deleted.HasLink())
|
|
|
|
repo := audit_model.EntityRef{Type: audit_model.ScopeRepository, ID: 3, Name: "Test User/Test Repo"}
|
|
assert.Equal(t, "/Test%20User/Test%20Repo", repo.HomeLink())
|
|
assert.True(t, repo.HasLink())
|
|
}
|
|
|
|
func TestEncodeDecodeMetadata(t *testing.T) {
|
|
raw := audit_model.EncodeMetadata(metaPairs("repo_id", int64(42), "repo", "o/r"))
|
|
decoded := audit_model.DecodeMetadata(raw)
|
|
assert.InDelta(t, 42.0, decoded["repo_id"], 0) // json numbers decode as float64
|
|
assert.Equal(t, "o/r", decoded["repo"])
|
|
}
|
|
|
|
func TestDoerFromContext(t *testing.T) {
|
|
doer := &user_model.User{ID: 2, Name: "Doer"}
|
|
signedIn := &user_model.User{ID: 3, Name: "SignedIn"}
|
|
|
|
t.Run("NoActor", func(t *testing.T) {
|
|
assert.Nil(t, doerFromContext(context.Background()))
|
|
})
|
|
|
|
t.Run("WithDoer", func(t *testing.T) {
|
|
assert.Equal(t, doer, doerFromContext(WithDoer(context.Background(), doer)))
|
|
})
|
|
|
|
t.Run("SignedInUserOfRequest", func(t *testing.T) {
|
|
ctx := newRequestContext(t, signedIn)
|
|
assert.Equal(t, signedIn, doerFromContext(ctx))
|
|
})
|
|
|
|
t.Run("WithDoerWinsOverSignedInUser", func(t *testing.T) {
|
|
ctx := WithDoer(newRequestContext(t, signedIn), doer)
|
|
assert.Equal(t, doer, doerFromContext(ctx))
|
|
})
|
|
}
|
|
|
|
// An impersonated session must never pin an event on the impersonated user
|
|
// alone, otherwise an admin could act in someone else's name untraceably.
|
|
func TestImpersonatorRef(t *testing.T) {
|
|
admin := &user_model.User{ID: 1, Name: "Admin"}
|
|
impersonated := &user_model.User{ID: 2, Name: "Impersonated"}
|
|
|
|
rc := reqctx.NewRequestContextForTest(t)
|
|
rc.GetData()[middleware.ContextDataKeySignedUser] = impersonated
|
|
rc.GetData()[middleware.ContextDataKeyImpersonator] = admin
|
|
|
|
assert.Equal(t, admin, ImpersonatorFromContext(rc))
|
|
|
|
e := buildEvent(rc, RecordParams{
|
|
Action: audit_model.UserPassword,
|
|
Actor: actorRef(doerFromContext(rc)),
|
|
Impersonator: impersonatorRef(ImpersonatorFromContext(rc), doerFromContext(rc)),
|
|
Scope: ScopeFromUser(impersonated),
|
|
})
|
|
assert.Equal(t, int64(2), e.ActorID)
|
|
assert.Equal(t, &audit_model.EntityRef{Type: audit_model.ScopeUser, ID: 1, Name: "Admin"}, e.Impersonator())
|
|
|
|
// an admin acting as themselves is not an impersonation
|
|
assert.Nil(t, impersonatorRef(admin, admin))
|
|
assert.Nil(t, impersonatorRef(nil, impersonated))
|
|
}
|
|
|
|
// An unresolvable actor must still produce an event, so a missing entry point
|
|
// never silently drops security relevant records.
|
|
func TestActorRefWithoutDoer(t *testing.T) {
|
|
ref := actorRef(nil)
|
|
assert.Equal(t, "Unknown", ref.DisplayName())
|
|
assert.False(t, ref.HasLink())
|
|
}
|
|
|
|
func TestRenderMessage(t *testing.T) {
|
|
actor := audit_model.EntityRef{Type: audit_model.ScopeUser, ID: 1, Name: "Actor"}
|
|
scope := audit_model.EntityRef{Type: audit_model.ScopeRepository, ID: 2, Name: "owner/repo"}
|
|
|
|
t.Run("EveryActionHasATemplate", func(t *testing.T) {
|
|
for _, action := range audit_model.AllActions() {
|
|
tmpl, ok := audit_model.MessageTemplate(action)
|
|
assert.True(t, ok, "action %q has no message template", action)
|
|
assert.NotEmpty(t, tmpl, "action %q has empty message template", action)
|
|
}
|
|
})
|
|
|
|
t.Run("ReservedPlaceholders", func(t *testing.T) {
|
|
assert.Equal(t,
|
|
"User Actor started impersonating user owner/repo.",
|
|
renderMessage(audit_model.UserImpersonation, actor, scope, nil),
|
|
)
|
|
})
|
|
|
|
t.Run("NonStringMetadata", func(t *testing.T) {
|
|
assert.Equal(t,
|
|
"Removed external login from authentication source 7 for user owner/repo.",
|
|
renderMessage(audit_model.UserExternalLoginRemove, actor, scope, map[string]any{"auth_source_id": int64(7)}),
|
|
)
|
|
})
|
|
|
|
t.Run("MissingMetadataKeepsTheKey", func(t *testing.T) {
|
|
assert.Equal(t,
|
|
"Added deploy key deploy_key for repository owner/repo.",
|
|
renderMessage(audit_model.RepositoryDeployKeyAdd, actor, scope, nil),
|
|
)
|
|
})
|
|
|
|
t.Run("UnknownActionFallsBackToItsName", func(t *testing.T) {
|
|
assert.Equal(t, "not:an:action", renderMessage(audit_model.Action("not:an:action"), actor, scope, nil))
|
|
})
|
|
}
|
|
|
|
func TestActionFilters(t *testing.T) {
|
|
assert.True(t, audit_model.IsActionFilter("user:impersonation"))
|
|
assert.True(t, audit_model.IsActionFilter(audit_model.UserImpersonation))
|
|
assert.False(t, audit_model.IsActionFilter("user:unknown"))
|
|
}
|