mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-14 19:22:09 +09:00
feat: Add audit logging (#38189)
Co-authored-by: bircni <bircni@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -7,12 +7,14 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
@@ -74,6 +76,8 @@ func CreateOrg(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.OrganizationCreate, org.AsUser())
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/user"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/mailer"
|
||||
@@ -152,6 +154,8 @@ func CreateUser(ctx *context.APIContext) {
|
||||
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserCreate, u)
|
||||
|
||||
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
// Send email notification.
|
||||
|
||||
@@ -68,6 +68,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
@@ -95,6 +96,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/user"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/actions"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -124,7 +126,13 @@ func sudo() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
log.Trace("Sudo from (%s) to: %s", ctx.Doer.Name, user.Name)
|
||||
|
||||
audit.Record(ctx, audit_model.UserImpersonation, user)
|
||||
|
||||
ctx.Doer = user
|
||||
// keep the audit actor in step with the effective doer, and keep the admin attached to it
|
||||
ctx.Data[middleware.ContextDataKeyImpersonator] = ctx.Data[middleware.ContextDataKeySignedUser]
|
||||
ctx.Data[middleware.ContextDataKeySignedUser] = user
|
||||
} else {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only administrators allowed to sudo.",
|
||||
@@ -1021,6 +1029,7 @@ func Routes() *web.Router {
|
||||
}
|
||||
|
||||
m.AfterRouting(context.APIContexter())
|
||||
m.AfterRouting(common.AuditOrigin(audit_model.OriginAPI))
|
||||
m.AfterRouting(checkDeprecatedAuthMethods)
|
||||
|
||||
// Get user from session if logged in.
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/shared"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
secret_service "gitea.dev/services/secrets"
|
||||
)
|
||||
@@ -108,12 +109,18 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
|
||||
|
||||
opt := web.GetForm[*api.CreateOrUpdateSecretOption](ctx)
|
||||
|
||||
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
|
||||
s, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
actions := audit.SecretUpdate
|
||||
if created {
|
||||
actions = audit.SecretAdd
|
||||
}
|
||||
audit.RecordScoped(ctx, ctx.Org.Organization.AsUser(), nil, actions, "secret", s.Name)
|
||||
|
||||
if created {
|
||||
ctx.Status(http.StatusCreated)
|
||||
} else {
|
||||
@@ -149,12 +156,14 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"))
|
||||
s, err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"))
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.RecordScoped(ctx, ctx.Org.Organization.AsUser(), nil, audit.SecretRemove, "secret", s.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/user"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
feed_service "gitea.dev/services/feed"
|
||||
@@ -296,6 +298,8 @@ func Create(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.OrganizationCreate, org.AsUser())
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/shared"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
secret_service "gitea.dev/services/secrets"
|
||||
@@ -137,12 +138,18 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
|
||||
|
||||
opt := web.GetForm[*api.CreateOrUpdateSecretOption](ctx)
|
||||
|
||||
_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
|
||||
s, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
actions := audit.SecretUpdate
|
||||
if created {
|
||||
actions = audit.SecretAdd
|
||||
}
|
||||
audit.RecordScoped(ctx, nil, repo, actions, "secret", s.Name)
|
||||
|
||||
if created {
|
||||
ctx.Status(http.StatusCreated)
|
||||
} else {
|
||||
@@ -185,12 +192,14 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname"))
|
||||
s, err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname"))
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.RecordScoped(ctx, nil, repo, audit.SecretRemove, "secret", s.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
feed_service "gitea.dev/services/feed"
|
||||
@@ -729,6 +731,10 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
return err
|
||||
}
|
||||
|
||||
if visibilityChanged {
|
||||
audit.Record(ctx, audit_model.RepositoryVisibility, repo, "visibility", repo.IsPrivate)
|
||||
}
|
||||
|
||||
if updateRepoLicense {
|
||||
if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/shared"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
secret_service "gitea.dev/services/secrets"
|
||||
)
|
||||
@@ -50,12 +51,18 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
|
||||
|
||||
opt := web.GetForm[*api.CreateOrUpdateSecretOption](ctx)
|
||||
|
||||
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
|
||||
s, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
actions := audit.SecretUpdate
|
||||
if created {
|
||||
actions = audit.SecretAdd
|
||||
}
|
||||
audit.RecordScoped(ctx, ctx.Doer, nil, actions, "secret", s.Name)
|
||||
|
||||
if created {
|
||||
ctx.Status(http.StatusCreated)
|
||||
} else {
|
||||
@@ -86,12 +93,14 @@ func DeleteSecret(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"))
|
||||
s, err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"))
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.RecordScoped(ctx, ctx.Doer, nil, audit.SecretRemove, "secret", s.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -149,6 +151,9 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserAccessTokenAdd, ctx.ContextUser, "token", t.Name, "token_scope", t.Scope)
|
||||
|
||||
ctx.JSON(http.StatusCreated, &api.AccessToken{
|
||||
Name: t.Name,
|
||||
Token: t.Token,
|
||||
@@ -211,11 +216,19 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil {
|
||||
t, err := auth_model.GetAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth_model.DeleteAccessTokenByID(ctx, t.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserAccessTokenRemove, ctx.ContextUser, "token", t.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -261,6 +274,8 @@ func CreateOauth2Application(ctx *context.APIContext) {
|
||||
}
|
||||
app.ClientSecret = secret
|
||||
|
||||
audit.Record(ctx, audit_model.UserOAuth2ApplicationAdd, ctx.Doer, "oauth2_application", app.Name)
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToOAuth2Application(app))
|
||||
}
|
||||
|
||||
@@ -323,6 +338,15 @@ func DeleteOauth2Application(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
appID := ctx.PathParamInt64("id")
|
||||
app, err := auth_model.GetOAuth2ApplicationByID(ctx, appID)
|
||||
if err != nil {
|
||||
if auth_model.IsErrOAuthApplicationNotFound(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := auth_model.DeleteOAuth2Application(ctx, appID, ctx.Doer.ID); err != nil {
|
||||
if auth_model.IsErrOAuthApplicationNotFound(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
@@ -332,6 +356,8 @@ func DeleteOauth2Application(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserOAuth2ApplicationRemove, ctx.Doer, "oauth2_application", app.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -430,5 +456,7 @@ func UpdateOauth2Application(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserOAuth2ApplicationUpdate, ctx.Doer, "oauth2_application", app.Name)
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToOAuth2Application(app))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/reqctx"
|
||||
audit_service "gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// AuditOrigin publishes the origin and the client address of the request, so
|
||||
// audit events recorded while serving it are attributed to it.
|
||||
func AuditOrigin(origin audit_model.Origin) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
if store := reqctx.GetRequestDataStore(req.Context()); store != nil {
|
||||
audit_service.SetRequestInfo(store, origin, httplib.RemoteHost(req))
|
||||
}
|
||||
next.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package common
|
||||
|
||||
import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
@@ -30,6 +31,18 @@ func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authM
|
||||
ctx.Data[middleware.ContextDataKeySignedUser] = ar.Doer
|
||||
ctx.Data["SignedUserID"] = ar.Doer.ID
|
||||
ctx.Data["IsAdmin"] = ar.Doer.IsAdmin
|
||||
|
||||
if sessionStore != nil {
|
||||
if uid := auth_service.ImpersonatorUserID(sessionStore); uid != 0 {
|
||||
impersonator, err := user_model.GetUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
// the session stays usable, but audit events must not silently lose the admin behind it
|
||||
log.Error("Unable to resolve impersonator %d: %v", uid, err)
|
||||
} else {
|
||||
ctx.Data[middleware.ContextDataKeyImpersonator] = impersonator
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.Data["SignedUserID"] = int64(0)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
db_install "gitea.dev/models/db/install"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/audit"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -508,6 +510,8 @@ func saveConfigAndRestart(ctx *context.Context, cfg setting.ConfigProvider, form
|
||||
u, _ = user_model.GetUserByName(ctx, u.Name)
|
||||
}
|
||||
|
||||
audit.RecordAs(ctx, u, audit_model.UserCreate, u)
|
||||
|
||||
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateAuthTokenForUserID", err)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
@@ -154,6 +156,8 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts
|
||||
repo.IsPrivate = isPrivate.Value()
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
|
||||
log.Error("failed to update repo is_private: %v", err)
|
||||
} else {
|
||||
audit.RecordAs(ctx, ctx.Doer, audit_model.RepositoryVisibility, repo, "visibility", repo.IsPrivate)
|
||||
}
|
||||
}
|
||||
if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -74,6 +75,7 @@ func Routes() *web.Router {
|
||||
// Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
|
||||
// Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers.
|
||||
r.AfterRouting(setRealIP)
|
||||
r.AfterRouting(common.AuditOrigin(audit_model.OriginSystem))
|
||||
|
||||
r.Get("/dummy", misc.DummyOK)
|
||||
r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
|
||||
|
||||
@@ -21,7 +21,7 @@ var (
|
||||
|
||||
func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
|
||||
return &user_setting.OAuth2CommonHandlers{
|
||||
OwnerID: 0,
|
||||
Owner: nil, // instance-wide
|
||||
BasePathList: setting.AppSubURL + "/-/admin/applications",
|
||||
BasePathEditPrefix: setting.AppSubURL + "/-/admin/applications/oauth2",
|
||||
TplAppEdit: tplSettingsOauth2ApplicationEdit,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
shared_audit "gitea.dev/routers/web/shared/audit"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const auditExportPageSize = 1000
|
||||
|
||||
func ViewAuditLogs(ctx *context.Context) {
|
||||
shared_audit.View(ctx, shared_audit.ViewOptions{
|
||||
Template: "admin/audit/list",
|
||||
PageData: map[string]any{"PageIsAdminMonitorAudit": true},
|
||||
})
|
||||
}
|
||||
|
||||
func ExportAuditLogs(ctx *context.Context) {
|
||||
// the export mirrors the filters of the listing it was started from
|
||||
searchOpts := shared_audit.SearchOptionsFromRequest(ctx, "", 0)
|
||||
searchOpts.Sort = audit_model.SortTimestampAsc
|
||||
searchOpts.PageSize = auditExportPageSize
|
||||
|
||||
page := 1
|
||||
findPage := func() ([]*audit_model.Event, int64, error) {
|
||||
searchOpts.Page = page
|
||||
return audit.FindEvents(ctx, searchOpts)
|
||||
}
|
||||
|
||||
// the first page is fetched before any header is written so a failing query still results in a proper error page
|
||||
events, total, err := findPage()
|
||||
if err != nil {
|
||||
ctx.ServerError("FindEvents", err)
|
||||
return
|
||||
}
|
||||
|
||||
httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{
|
||||
ContentType: "application/x-ndjson; charset=utf-8",
|
||||
Filename: fmt.Sprintf("gitea-audit-log-%s.jsonl", time.Now().UTC().Format("20060102-150405Z")),
|
||||
ContentDisposition: httplib.ContentDispositionAttachment,
|
||||
})
|
||||
ctx.SetTotalCountHeader(total) // lets a client detect an export truncated by a mid-stream failure
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
|
||||
for {
|
||||
if err := audit.WriteEventsAsJSON(ctx.Resp, events); err != nil {
|
||||
log.Debug("Unable to write audit log export: %v", err)
|
||||
return
|
||||
}
|
||||
if len(events) < auditExportPageSize {
|
||||
return
|
||||
}
|
||||
|
||||
page++
|
||||
events, _, err = findPage()
|
||||
if err != nil {
|
||||
log.Error("Unable to continue audit log export: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.CreateSource(ctx, &auth.Source{
|
||||
if err := auth_service.CreateSource(ctx, &auth.Source{
|
||||
Type: auth.Type(form.Type),
|
||||
Name: form.Name,
|
||||
IsActive: form.IsActive,
|
||||
@@ -419,7 +419,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
source.IsSyncEnabled = form.IsSyncEnabled
|
||||
source.Cfg = config
|
||||
source.TwoFactorPolicy = form.TwoFactorPolicy
|
||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||
if err := auth_service.UpdateSource(ctx, source); err != nil {
|
||||
if errExist, ok := errors.AsType[auth.ErrSourceAlreadyExist](err); ok {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", errExist.Name), tplAuthEdit, form)
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/user"
|
||||
)
|
||||
@@ -129,6 +131,13 @@ func ActivateEmail(ctx *context.Context) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
|
||||
}
|
||||
} else {
|
||||
// the email already changed, so a failed lookup must not fail the request
|
||||
if u, err := user_model.GetUserByID(ctx, uid); err != nil {
|
||||
log.Error("GetUserByID(%d) for audit: %v", uid, err)
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserEmailActivate, u, "email", email, "activated", activate)
|
||||
}
|
||||
|
||||
log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
|
||||
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
@@ -61,9 +62,14 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
|
||||
|
||||
// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
|
||||
func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteDefaultSystemWebhook(ctx, ctx.FormInt64("id")); err != nil {
|
||||
hook, err := webhook.GetWebhookByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetWebhookByID: " + err.Error())
|
||||
} else if err := webhook.DeleteDefaultSystemWebhook(ctx, hook.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
|
||||
} else {
|
||||
audit.RecordScoped(ctx, nil, nil, audit.WebhookRemove, "webhook", hook.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/explore"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/audit"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -203,6 +205,8 @@ func NewUserPost(ctx *context.Context) {
|
||||
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserCreate, u)
|
||||
|
||||
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
// Send email notification.
|
||||
@@ -468,6 +472,7 @@ func ImpersonateUser(ctx *context.Context) {
|
||||
ctx.ServerError("unable to impersonate user", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.UserImpersonation, u)
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings")
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -82,6 +84,10 @@ func TwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if u, err := user_model.GetUserByID(ctx, id); err == nil {
|
||||
audit.RecordAs(ctx, u, audit_model.UserAuthenticationFailTwoFactor, u)
|
||||
}
|
||||
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/services/audit"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
@@ -453,12 +455,15 @@ func SignOut(ctx *context.Context) {
|
||||
websocket_service.PublishLogout(ctx.Doer.ID, ctx.Session.ID())
|
||||
}
|
||||
|
||||
impersonator := audit.ImpersonatorFromContext(ctx)
|
||||
|
||||
exitedImpersonated, err := auth_service.ExitImpersonatedUser(ctx.Session)
|
||||
if err != nil {
|
||||
ctx.ServerError("ExitImpersonatedUser", err)
|
||||
return
|
||||
}
|
||||
if exitedImpersonated {
|
||||
audit.RecordAs(ctx, impersonator, audit_model.UserImpersonationExit, ctx.Doer)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"gitea.dev/modules/proxy"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/audit"
|
||||
source_service "gitea.dev/services/auth/source"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
@@ -419,7 +420,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
// Register last login
|
||||
opts.SetLastLogin = true
|
||||
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
if err := user_service.UpdateUser(audit.WithDoer(ctx, user_model.NewAuthenticationSourceUser()), u, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
@@ -448,7 +449,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
}
|
||||
|
||||
if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() {
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
if err := user_service.UpdateUser(audit.WithDoer(ctx, user_model.NewAuthenticationSourceUser()), u, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/httpauth"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -120,7 +122,7 @@ func InfoOAuth(ctx *context.Context) {
|
||||
var accessTokenScope auth.AccessTokenScope
|
||||
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
|
||||
if parsed, ok := httpauth.ParseAuthorizationHeader(auHead); ok && parsed.BearerToken != nil {
|
||||
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token)
|
||||
accessTokenScope, _, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +429,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
|
||||
}, form.RedirectURI)
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserOAuth2ApplicationGrant, ctx.Doer, "oauth2_application", app.Name, "granted_scope", form.Scope)
|
||||
} else if grant.Scope != form.Scope {
|
||||
handleAuthorizeError(ctx, AuthorizeError{
|
||||
State: form.State,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
user_service "gitea.dev/services/user"
|
||||
@@ -54,7 +55,7 @@ func oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.Us
|
||||
// sync user flags (admin/restricted)
|
||||
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
|
||||
if isAdmin.Has() || isRestricted.Has() {
|
||||
if err = user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{IsAdmin: isAdmin, IsRestricted: isRestricted}); err != nil {
|
||||
if err = user_service.UpdateUser(audit.WithDoer(ctx, user_model.NewAuthenticationSourceUser()), u, &user_service.UpdateOptions{IsAdmin: isAdmin, IsRestricted: isRestricted}); err != nil {
|
||||
log.Error("Unable to sync OAuth2 user admin or restricted status %s: %v", gothUser.Provider, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/password"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/mailer"
|
||||
@@ -86,6 +88,10 @@ func ForgotPasswdPost(ctx *context.Context) {
|
||||
|
||||
mailer.SendResetPasswordMail(u)
|
||||
|
||||
// the request is unauthenticated, so the affected account is the only actor
|
||||
// we can name; the recorded IP address carries the forensic signal
|
||||
audit.RecordAs(ctx, u, audit_model.UserPasswordResetRequest, u)
|
||||
|
||||
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -79,6 +81,8 @@ func CreatePost(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.OrganizationCreate, org.AsUser())
|
||||
|
||||
log.Trace("Organization created: %s", org.Name)
|
||||
|
||||
ctx.Redirect(org.AsUser().DashboardLink())
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
project_model "gitea.dev/models/project"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/shared/issue"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
project_service "gitea.dev/services/projects"
|
||||
@@ -171,6 +173,7 @@ func NewProjectPost(ctx *context.Context) {
|
||||
ctx.ServerError("NewProject", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.ProjectCreate, ctx.ContextUser, "project", newProject.Title, "project_id", newProject.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
|
||||
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
|
||||
@@ -214,6 +217,7 @@ func DeleteProject(ctx *context.Context) {
|
||||
if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.ProjectDelete, ctx.ContextUser, "project", p.Title, "project_id", p.ID)
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
|
||||
}
|
||||
|
||||
@@ -284,6 +288,7 @@ func EditProjectPost(ctx *context.Context) {
|
||||
ctx.ServerError("UpdateProjects", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.ProjectUpdate, ctx.ContextUser, "project", p.Title, "project_id", p.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
|
||||
if ctx.FormString("redirect") == "project" {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
org_service "gitea.dev/services/org"
|
||||
@@ -172,9 +173,14 @@ func Webhooks(ctx *context.Context) {
|
||||
|
||||
// DeleteWebhook response for delete webhook
|
||||
func DeleteWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
|
||||
hook, err := webhook.GetWebhookByOwnerID(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetWebhookByOwnerID: " + err.Error())
|
||||
} else if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Org.Organization.ID, hook.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error())
|
||||
} else {
|
||||
audit.RecordScoped(ctx, ctx.Org.Organization.AsUser(), nil, audit.WebhookRemove, "webhook", hook.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
audit_model "gitea.dev/models/audit"
|
||||
shared_audit "gitea.dev/routers/web/shared/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func ViewAuditLogs(ctx *context.Context) {
|
||||
shared_audit.View(ctx, shared_audit.ViewOptions{
|
||||
Template: "org/settings/audit_logs",
|
||||
ScopeType: audit_model.ScopeOrganization,
|
||||
ScopeID: ctx.Org.Organization.ID,
|
||||
PageData: map[string]any{
|
||||
"PageIsOrgSettings": true,
|
||||
"PageIsSettingsAudit": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
|
||||
func newOAuth2CommonHandlers(org *context.Organization) *user_setting.OAuth2CommonHandlers {
|
||||
return &user_setting.OAuth2CommonHandlers{
|
||||
OwnerID: org.Organization.ID,
|
||||
Owner: org.Organization.AsUser(),
|
||||
BasePathList: fmt.Sprintf("%s/org/%s/settings/applications", setting.AppSubURL, org.Organization.Name),
|
||||
BasePathEditPrefix: fmt.Sprintf("%s/org/%s/settings/applications/oauth2", setting.AppSubURL, org.Organization.Name),
|
||||
TplAppEdit: tplSettingsOAuthApplicationEdit,
|
||||
|
||||
@@ -1385,6 +1385,7 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
|
||||
ctx.ServerError("UpdateRepoUnit", err)
|
||||
return
|
||||
}
|
||||
actions_service.RecordWorkflowToggle(ctx, ctx.Repo.Repository, workflow, isEnable)
|
||||
|
||||
if isEnable {
|
||||
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
project_model "gitea.dev/models/project"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/shared/issue"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
project_service "gitea.dev/services/projects"
|
||||
@@ -134,7 +136,7 @@ func NewProjectPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.NewProject(ctx, &project_model.Project{
|
||||
project := &project_model.Project{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Title: form.Title,
|
||||
Description: form.Content,
|
||||
@@ -142,10 +144,12 @@ func NewProjectPost(ctx *context.Context) {
|
||||
TemplateType: form.TemplateType,
|
||||
CardType: form.CardType,
|
||||
Type: project_model.TypeRepository,
|
||||
}); err != nil {
|
||||
}
|
||||
if err := project_model.NewProject(ctx, project); err != nil {
|
||||
ctx.ServerError("NewProject", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.ProjectCreate, ctx.Repo.Repository, "project", project.Title, "project_id", project.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
|
||||
@@ -191,6 +195,7 @@ func DeleteProject(ctx *context.Context) {
|
||||
if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.ProjectDelete, ctx.Repo.Repository, "project", p.Title, "project_id", p.ID)
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
|
||||
}
|
||||
|
||||
@@ -265,6 +270,7 @@ func EditProjectPost(ctx *context.Context) {
|
||||
ctx.ServerError("UpdateProjects", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.ProjectUpdate, ctx.Repo.Repository, "project", p.Title, "project_id", p.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
|
||||
if ctx.FormString("redirect") == "project" {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
audit_model "gitea.dev/models/audit"
|
||||
shared_audit "gitea.dev/routers/web/shared/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func ViewAuditLogs(ctx *context.Context) {
|
||||
shared_audit.View(ctx, shared_audit.ViewOptions{
|
||||
Template: "repo/settings/audit_logs",
|
||||
ScopeType: audit_model.ScopeRepository,
|
||||
ScopeID: ctx.Repo.Repository.ID,
|
||||
PageData: map[string]any{"PageIsSettingsAudit": true},
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
deploykey_model "gitea.dev/models/deploykey"
|
||||
"gitea.dev/models/perm"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -68,6 +70,7 @@ func DeployKeysPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryDeployKeyAdd, ctx.Repo.Repository, "deploy_key", key.Name)
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/repo"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
@@ -149,8 +151,10 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
isNewProtectedBranch := false
|
||||
if protectBranch == nil {
|
||||
// No options found, create defaults.
|
||||
isNewProtectedBranch = true
|
||||
protectBranch = &git_model.ProtectedBranch{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
RuleName: f.RuleName,
|
||||
@@ -291,6 +295,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if isNewProtectedBranch {
|
||||
audit.Record(ctx, audit_model.RepositoryBranchProtectionAdd, ctx.Repo.Repository, "rule", protectBranch.RuleName)
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.RepositoryBranchProtectionUpdate, ctx.Repo.Repository, "rule", protectBranch.RuleName)
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.RuleName))
|
||||
ctx.Redirect(fmt.Sprintf("%s/settings/branches?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
|
||||
}
|
||||
@@ -323,6 +333,8 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryBranchProtectionRemove, ctx.Repo.Repository, "rule", rule.RuleName)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -65,6 +67,8 @@ func NewProtectedTagPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryTagProtectionAdd, ctx.Repo.Repository, "pattern", pt.NamePattern)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
}
|
||||
@@ -118,6 +122,8 @@ func EditProtectedTagPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryTagProtectionUpdate, ctx.Repo.Repository, "pattern", pt.NamePattern)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
|
||||
}
|
||||
@@ -134,6 +140,8 @@ func DeleteProtectedTagPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryTagProtectionRemove, ctx.Repo.Repository, "pattern", pt.NamePattern)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
shared "gitea.dev/routers/web/shared/secrets"
|
||||
@@ -22,6 +24,8 @@ const (
|
||||
)
|
||||
|
||||
type secretsCtx struct {
|
||||
Owner *user_model.User
|
||||
Repo *repo_model.Repository
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
@@ -34,7 +38,7 @@ type secretsCtx struct {
|
||||
func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &secretsCtx{
|
||||
OwnerID: 0,
|
||||
Repo: ctx.Repo.Repository,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsRepo: true,
|
||||
SecretsTemplate: tplRepoSecrets,
|
||||
@@ -48,8 +52,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
|
||||
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
|
||||
}
|
||||
return &secretsCtx{
|
||||
Owner: ctx.ContextUser,
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
RepoID: 0,
|
||||
IsOrg: true,
|
||||
SecretsTemplate: tplOrgSecrets,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/secrets",
|
||||
@@ -58,8 +62,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &secretsCtx{
|
||||
Owner: ctx.Doer,
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
SecretsTemplate: tplUserSecrets,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/secrets",
|
||||
@@ -105,8 +109,8 @@ func SecretsPost(ctx *context.Context) {
|
||||
|
||||
shared.PerformSecretsPost(
|
||||
ctx,
|
||||
sCtx.OwnerID,
|
||||
sCtx.RepoID,
|
||||
sCtx.Owner,
|
||||
sCtx.Repo,
|
||||
sCtx.RedirectLink,
|
||||
)
|
||||
}
|
||||
@@ -119,8 +123,8 @@ func SecretsDelete(ctx *context.Context) {
|
||||
}
|
||||
shared.PerformSecretsDelete(
|
||||
ctx,
|
||||
sCtx.OwnerID,
|
||||
sCtx.RepoID,
|
||||
sCtx.Owner,
|
||||
sCtx.Repo,
|
||||
sCtx.RedirectLink,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
repo_router "gitea.dev/routers/web/repo"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/migrations"
|
||||
@@ -478,6 +480,8 @@ func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryMirrorPushRemove, repo, "mirror_id", m.ID, "remote_address", m.RemoteAddress)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(repo.Link() + "/settings")
|
||||
}
|
||||
@@ -542,6 +546,8 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryMirrorPushAdd, repo, "mirror_id", m.ID, "remote_address", m.RemoteAddress)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(repo.Link() + "/settings")
|
||||
}
|
||||
@@ -724,6 +730,9 @@ func handleSettingsPostSigning(ctx *context.Context) {
|
||||
ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositorySigningVerification, repo, "trust_model", repo.TrustModel.String())
|
||||
|
||||
log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
}
|
||||
|
||||
@@ -808,6 +817,9 @@ func handleSettingsPostConvert(ctx *context.Context) {
|
||||
ctx.ServerError("DeleteMirrorByRepoID", err)
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryConvertMirror, repo)
|
||||
|
||||
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
|
||||
ctx.JSONRedirect(repo.Link())
|
||||
@@ -1018,6 +1030,8 @@ func handleSettingsPostArchive(ctx *context.Context) {
|
||||
// update issue indexer
|
||||
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryArchive, repo)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
|
||||
|
||||
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
@@ -1046,6 +1060,8 @@ func handleSettingsPostUnarchive(ctx *context.Context) {
|
||||
// update issue indexer
|
||||
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
|
||||
|
||||
audit.Record(ctx, audit_model.RepositoryUnarchive, repo)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
|
||||
|
||||
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -58,6 +60,8 @@ func Webhooks(ctx *context.Context) {
|
||||
}
|
||||
|
||||
type ownerRepoCtx struct {
|
||||
Owner *user_model.User
|
||||
Repo *repo_model.Repository
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsAdmin bool
|
||||
@@ -71,6 +75,7 @@ type ownerRepoCtx struct {
|
||||
func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &ownerRepoCtx{
|
||||
Repo: ctx.Repo.Repository,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
|
||||
LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
|
||||
@@ -80,6 +85,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
return &ownerRepoCtx{
|
||||
Owner: ctx.ContextUser,
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
Link: path.Join(ctx.Org.OrgLink, "settings/hooks"),
|
||||
LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"),
|
||||
@@ -89,6 +95,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &ownerRepoCtx{
|
||||
Owner: ctx.Doer,
|
||||
OwnerID: ctx.Doer.ID,
|
||||
Link: path.Join(setting.AppSubURL, "/user/settings/hooks"),
|
||||
LinkNew: path.Join(setting.AppSubURL, "/user/settings/hooks"),
|
||||
@@ -109,6 +116,14 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
|
||||
return nil, errors.New("unable to set OwnerRepo context")
|
||||
}
|
||||
|
||||
// recordWebhookAudit emits a webhook audit event scoped to the repository,
|
||||
// organization, user, or instance (admin/system) the webhook belongs to. The
|
||||
// shared add/edit handlers run in any of these contexts, so the scope is derived
|
||||
// from orCtx rather than assuming a repository.
|
||||
func (orCtx *ownerRepoCtx) recordWebhookAudit(ctx *context.Context, actions audit.ScopedActions, url string) {
|
||||
audit.RecordScoped(ctx, orCtx.Owner, orCtx.Repo, actions, "webhook", url)
|
||||
}
|
||||
|
||||
func checkHookType(ctx *context.Context) string {
|
||||
hookType := strings.ToLower(ctx.PathParam("type"))
|
||||
if !util.SliceContainsString(setting.Webhook.Types, hookType, true) {
|
||||
@@ -258,6 +273,8 @@ func createWebhook(ctx *context.Context, params webhookParams) {
|
||||
return
|
||||
}
|
||||
|
||||
orCtx.recordWebhookAudit(ctx, audit.WebhookAdd, w.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
|
||||
ctx.Redirect(orCtx.Link)
|
||||
}
|
||||
@@ -311,6 +328,8 @@ func editWebhook(ctx *context.Context, params webhookParams) {
|
||||
return
|
||||
}
|
||||
|
||||
orCtx.recordWebhookAudit(ctx, audit.WebhookUpdate, w.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
|
||||
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
|
||||
}
|
||||
@@ -736,9 +755,14 @@ func ReplayWebhook(ctx *context.Context) {
|
||||
|
||||
// DeleteWebhook delete a webhook
|
||||
func DeleteWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
|
||||
hook, err := webhook.GetWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetWebhookByRepoID: " + err.Error())
|
||||
} else if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, hook.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
|
||||
} else {
|
||||
audit.RecordScoped(ctx, nil, ctx.Repo.Repository, audit.WebhookRemove, "webhook", hook.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// ViewOptions configures a scoped audit log listing. The admin view leaves
|
||||
// ScopeType empty to list every event; the user/org/repo views constrain the
|
||||
// query to their own scope.
|
||||
type ViewOptions struct {
|
||||
Template templates.TplName
|
||||
ScopeType audit_model.ScopeType
|
||||
ScopeID int64
|
||||
// PageData holds ctx.Data flags to enable for the active navigation tab.
|
||||
PageData map[string]any
|
||||
}
|
||||
|
||||
var filterableOrigins = []audit_model.Origin{audit_model.OriginUI, audit_model.OriginAPI, audit_model.OriginCLI, audit_model.OriginSystem}
|
||||
|
||||
// SearchOptionsFromRequest builds the event query from the request filters and
|
||||
// publishes the applied values to ctx.Data so the filter form can render its
|
||||
// current state. The listing and the export share it to stay in sync.
|
||||
func SearchOptionsFromRequest(ctx *context.Context, scopeType audit_model.ScopeType, scopeID int64) *audit_model.EventSearchOptions {
|
||||
// only the two known sort values are accepted, anything else falls back to the default
|
||||
sort := util.Iif(audit_model.EventSort(ctx.FormString("sort")) == audit_model.SortTimestampAsc, audit_model.SortTimestampAsc, audit_model.SortTimestampDesc)
|
||||
|
||||
opts := &audit_model.EventSearchOptions{
|
||||
Sort: sort,
|
||||
ScopeType: scopeType,
|
||||
ScopeID: scopeID,
|
||||
}
|
||||
|
||||
if action := audit_model.Action(ctx.FormString("action")); action != "" {
|
||||
if _, ok := audit_model.MessageTemplate(action); ok {
|
||||
opts.Action = action
|
||||
} else if audit_model.IsActionFilter(action) {
|
||||
opts.ActionPrefix = action
|
||||
}
|
||||
}
|
||||
if origin := audit_model.Origin(ctx.FormString("origin")); slices.Contains(filterableOrigins, origin) {
|
||||
opts.Origin = origin
|
||||
}
|
||||
if actor := ctx.FormTrim("actor"); actor != "" {
|
||||
u, err := user_model.GetUserByName(ctx, actor)
|
||||
if err != nil {
|
||||
opts.ActorID = -1 // an unknown actor matches nothing rather than everything
|
||||
} else {
|
||||
opts.ActorID = u.ID
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["AuditSort"] = string(opts.Sort)
|
||||
ctx.Data["AuditFilterAction"] = string(opts.Action)
|
||||
if opts.ActionPrefix != "" {
|
||||
ctx.Data["AuditFilterAction"] = string(opts.ActionPrefix)
|
||||
}
|
||||
ctx.Data["AuditFilterOrigin"] = string(opts.Origin)
|
||||
ctx.Data["AuditFilterActor"] = ctx.FormTrim("actor")
|
||||
ctx.Data["AuditActions"] = audit_model.ActionFilters()
|
||||
ctx.Data["AuditOrigins"] = filterableOrigins
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// View renders a paginated audit log listing shared by the admin, user, org and
|
||||
// repo settings pages. Only the scope filter, template and page flags differ.
|
||||
func View(ctx *context.Context, opts ViewOptions) {
|
||||
ctx.Data["Title"] = ctx.Tr("audit.title")
|
||||
ctx.Data["AuditRecordEnabled"] = setting.AuditRecordEnabled()
|
||||
maps.Copy(ctx.Data, opts.PageData)
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
searchOpts := SearchOptionsFromRequest(ctx, opts.ScopeType, opts.ScopeID)
|
||||
searchOpts.ListOptions = db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.Admin.NoticePagingNum,
|
||||
}
|
||||
|
||||
evs, total, err := audit.FindEvents(ctx, searchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindEvents", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["AuditEvents"] = evs
|
||||
|
||||
ctx.Data["Page"] = context.NewPagerBuilder(ctx).TotalCount(total).PerPageLimit(setting.UI.Admin.NoticePagingNum).CurPage(page).Build()
|
||||
|
||||
ctx.HTML(http.StatusOK, opts.Template)
|
||||
}
|
||||
@@ -5,10 +5,13 @@ package secrets
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
secret_model "gitea.dev/models/secret"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
secret_service "gitea.dev/services/secrets"
|
||||
@@ -26,29 +29,49 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
|
||||
ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength
|
||||
}
|
||||
|
||||
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
form := web.GetForm[*forms.AddSecretForm](ctx)
|
||||
func secretOwnerRepoIDs(owner *user_model.User, repo *repo_model.Repository) (ownerID, repoID int64) {
|
||||
if owner != nil {
|
||||
ownerID = owner.ID
|
||||
}
|
||||
if repo != nil {
|
||||
repoID = repo.ID
|
||||
}
|
||||
return ownerID, repoID
|
||||
}
|
||||
|
||||
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.NormalizeStringEOL(form.Data), form.Description)
|
||||
func PerformSecretsPost(ctx *context.Context, owner *user_model.User, repo *repo_model.Repository, redirectURL string) {
|
||||
form := web.GetForm[*forms.AddSecretForm](ctx)
|
||||
ownerID, repoID := secretOwnerRepoIDs(owner, repo)
|
||||
|
||||
s, created, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.NormalizeStringEOL(form.Data), form.Description)
|
||||
if err != nil {
|
||||
ctx.JSONErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
actions := audit.SecretUpdate
|
||||
if created {
|
||||
actions = audit.SecretAdd
|
||||
}
|
||||
audit.RecordScoped(ctx, owner, repo, actions, "secret", s.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.save_success", s.Name))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
func PerformSecretsDelete(ctx *context.Context, owner *user_model.User, repo *repo_model.Repository, redirectURL string) {
|
||||
id := ctx.FormInt64("id")
|
||||
ownerID, repoID := secretOwnerRepoIDs(owner, repo)
|
||||
|
||||
err := secret_service.DeleteSecretByID(ctx, ownerID, repoID, id)
|
||||
s, err := secret_service.DeleteSecretByID(ctx, ownerID, repoID, id)
|
||||
if err != nil {
|
||||
log.Error("DeleteSecretByID(%d) failed: %v", id, err)
|
||||
ctx.JSONError(ctx.Tr("secrets.deletion.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
audit.RecordScoped(ctx, owner, repo, audit.SecretRemove, "secret", s.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -104,6 +106,8 @@ func ApplicationsPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserAccessTokenAdd, ctx.Doer, "token", t.Name, "token_scope", t.Scope)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
|
||||
ctx.Flash.Info(t.Token)
|
||||
|
||||
@@ -112,9 +116,14 @@ func ApplicationsPost(ctx *context.Context) {
|
||||
|
||||
// DeleteApplication response for delete user access token
|
||||
func DeleteApplication(ctx *context.Context) {
|
||||
if err := auth_model.DeleteAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
|
||||
t, err := auth_model.GetAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetAccessTokenByID: " + err.Error())
|
||||
} else if err := auth_model.DeleteAccessTokenByID(ctx, t.ID, ctx.Doer.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserAccessTokenRemove, ctx.Doer, "token", t.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
audit_model "gitea.dev/models/audit"
|
||||
shared_audit "gitea.dev/routers/web/shared/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func ViewAuditLogs(ctx *context.Context) {
|
||||
shared_audit.View(ctx, shared_audit.ViewOptions{
|
||||
Template: "user/settings/audit_logs",
|
||||
ScopeType: audit_model.ScopeUser,
|
||||
ScopeID: ctx.Doer.ID,
|
||||
PageData: map[string]any{"PageIsSettingsAudit": true},
|
||||
})
|
||||
}
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
"net/http"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -68,7 +70,8 @@ func KeysPost(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
return
|
||||
}
|
||||
if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
|
||||
key, err := asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0)
|
||||
if err != nil {
|
||||
ctx.Data["HasPrincipalError"] = true
|
||||
switch {
|
||||
case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
|
||||
@@ -81,6 +84,9 @@ func KeysPost(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserKeyPrincipalAdd, ctx.Doer, "key", key.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
case "gpg":
|
||||
@@ -135,6 +141,8 @@ func KeysPost(ctx *context.Context) {
|
||||
for _, key := range keys {
|
||||
keyIDs += key.KeyID
|
||||
keyIDs += ", "
|
||||
|
||||
audit.Record(ctx, audit_model.UserKeyGPGAdd, ctx.Doer, "gpg_key_id", key.KeyID)
|
||||
}
|
||||
if len(keyIDs) > 0 {
|
||||
keyIDs = keyIDs[:len(keyIDs)-2]
|
||||
@@ -189,7 +197,8 @@ func KeysPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = asymkey_model.AddPublicKey(ctx, ctx.Doer.ID, form.Title, content, 0, false); err != nil {
|
||||
key, err := asymkey_model.AddPublicKey(ctx, ctx.Doer.ID, form.Title, content, 0, false)
|
||||
if err != nil {
|
||||
ctx.Data["HasSSHError"] = true
|
||||
switch {
|
||||
case asymkey_model.IsErrKeyAlreadyExist(err):
|
||||
@@ -210,6 +219,9 @@ func KeysPost(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserKeySSHAdd, ctx.Doer, "fingerprint", key.Fingerprint)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||
case "verify_ssh":
|
||||
@@ -256,10 +268,18 @@ func DeleteKey(ctx *context.Context) {
|
||||
ctx.JSONError("gpg keys setting is not allowed to be visited")
|
||||
return
|
||||
}
|
||||
key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
|
||||
if err != nil && !asymkey_model.IsErrGPGKeyNotExist(err) {
|
||||
ctx.ServerError("GetGPGKeyForUserByID", err)
|
||||
return
|
||||
}
|
||||
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
|
||||
ctx.JSONError("Failed to delete PGP key")
|
||||
return
|
||||
}
|
||||
if key != nil {
|
||||
audit.Record(ctx, audit_model.UserKeyGPGRemove, ctx.Doer, "gpg_key_id", key.KeyID)
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
|
||||
case "ssh":
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
@@ -13,9 +14,9 @@ const (
|
||||
tplSettingsOAuthApplicationEdit templates.TplName = "user/settings/applications_oauth2_edit"
|
||||
)
|
||||
|
||||
func newOAuth2CommonHandlers(userID int64) *OAuth2CommonHandlers {
|
||||
func newOAuth2CommonHandlers(owner *user_model.User) *OAuth2CommonHandlers {
|
||||
return &OAuth2CommonHandlers{
|
||||
OwnerID: userID,
|
||||
Owner: owner,
|
||||
BasePathList: setting.AppSubURL + "/user/settings/applications",
|
||||
BasePathEditPrefix: setting.AppSubURL + "/user/settings/applications/oauth2",
|
||||
TplAppEdit: tplSettingsOAuthApplicationEdit,
|
||||
@@ -27,7 +28,7 @@ func OAuthApplicationsPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer)
|
||||
oa.AddApp(ctx)
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ func OAuthApplicationsEdit(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer)
|
||||
oa.EditSave(ctx)
|
||||
}
|
||||
|
||||
@@ -45,24 +46,24 @@ func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer)
|
||||
oa.RegenerateSecret(ctx)
|
||||
}
|
||||
|
||||
// OAuth2ApplicationShow displays the given application
|
||||
func OAuth2ApplicationShow(ctx *context.Context) {
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer)
|
||||
oa.EditShow(ctx)
|
||||
}
|
||||
|
||||
// DeleteOAuth2Application deletes the given oauth2 application
|
||||
func DeleteOAuth2Application(ctx *context.Context) {
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer)
|
||||
oa.DeleteApp(ctx)
|
||||
}
|
||||
|
||||
// RevokeOAuth2Grant revokes the grant with the given id
|
||||
func RevokeOAuth2Grant(ctx *context.Context) {
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer.ID)
|
||||
oa := newOAuth2CommonHandlers(ctx.Doer)
|
||||
oa.RevokeGrant(ctx)
|
||||
}
|
||||
|
||||
@@ -8,21 +8,36 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
type OAuth2CommonHandlers struct {
|
||||
OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID
|
||||
Owner *user_model.User // nil for instance-wide, otherwise the Org or User owning the applications
|
||||
BasePathList string // the base URL for the application list page, eg: "/user/setting/applications"
|
||||
BasePathEditPrefix string // the base URL for the application edit page, will be appended with app id, eg: "/user/setting/applications/oauth2"
|
||||
TplAppEdit templates.TplName // the template for the application edit page
|
||||
}
|
||||
|
||||
func (oa *OAuth2CommonHandlers) ownerID() int64 {
|
||||
if oa.Owner != nil {
|
||||
return oa.Owner.ID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// recordAudit emits an OAuth2 application audit event scoped to the owner, which
|
||||
// is nil for instance-wide (admin) applications, an organization, or a user.
|
||||
func (oa *OAuth2CommonHandlers) recordAudit(ctx *context.Context, actions audit.ScopedActions, appName string) {
|
||||
audit.RecordScoped(ctx, oa.Owner, nil, actions, "oauth2_application", appName)
|
||||
}
|
||||
|
||||
func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context, app *auth.OAuth2Application) {
|
||||
ctx.Data["App"] = app
|
||||
ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID)
|
||||
@@ -50,7 +65,7 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
|
||||
app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
UserID: oa.OwnerID,
|
||||
UserID: oa.ownerID(),
|
||||
ConfidentialClient: form.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
|
||||
})
|
||||
@@ -59,6 +74,8 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
oa.recordAudit(ctx, audit.OAuth2ApplicationAdd, app.Name)
|
||||
|
||||
// render the edit page with secret
|
||||
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"), true)
|
||||
ctx.Data["ClientSecret"], err = app.GenerateClientSecret(ctx)
|
||||
@@ -81,7 +98,7 @@ func (oa *OAuth2CommonHandlers) EditShow(ctx *context.Context) {
|
||||
ctx.ServerError("GetOAuth2ApplicationByID", err)
|
||||
return
|
||||
}
|
||||
if app.UID != oa.OwnerID {
|
||||
if app.UID != oa.ownerID() {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
@@ -102,7 +119,7 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
|
||||
ctx.ServerError("GetOAuth2ApplicationByID", err)
|
||||
return
|
||||
}
|
||||
if app.UID != oa.OwnerID {
|
||||
if app.UID != oa.ownerID() {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
@@ -110,18 +127,21 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if ctx.Data["App"], err = auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
|
||||
updatedApp, err := auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
|
||||
ID: ctx.PathParamInt64("id"),
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
UserID: oa.OwnerID,
|
||||
UserID: oa.ownerID(),
|
||||
ConfidentialClient: form.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("UpdateOAuth2Application", err)
|
||||
return
|
||||
}
|
||||
|
||||
oa.recordAudit(ctx, audit.OAuth2ApplicationUpdate, updatedApp.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
|
||||
ctx.Redirect(oa.BasePathList)
|
||||
}
|
||||
@@ -137,7 +157,7 @@ func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) {
|
||||
ctx.ServerError("GetOAuth2ApplicationByID", err)
|
||||
return
|
||||
}
|
||||
if app.UID != oa.OwnerID {
|
||||
if app.UID != oa.ownerID() {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
@@ -146,28 +166,59 @@ func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) {
|
||||
ctx.ServerError("GenerateClientSecret", err)
|
||||
return
|
||||
}
|
||||
|
||||
oa.recordAudit(ctx, audit.OAuth2ApplicationSecret, app.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"), true)
|
||||
oa.renderEditPage(ctx, app)
|
||||
}
|
||||
|
||||
// DeleteApp deletes the given oauth2 application
|
||||
func (oa *OAuth2CommonHandlers) DeleteApp(ctx *context.Context) {
|
||||
if err := auth.DeleteOAuth2Application(ctx, ctx.PathParamInt64("id"), oa.OwnerID); err != nil {
|
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetOAuth2ApplicationByID", auth.IsErrOAuthApplicationNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.DeleteOAuth2Application(ctx, app.ID, oa.ownerID()); err != nil {
|
||||
ctx.ServerError("DeleteOAuth2Application", err)
|
||||
return
|
||||
}
|
||||
|
||||
oa.recordAudit(ctx, audit.OAuth2ApplicationRemove, app.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
|
||||
ctx.JSONRedirect(oa.BasePathList)
|
||||
}
|
||||
|
||||
// RevokeGrant revokes the grant
|
||||
func (oa *OAuth2CommonHandlers) RevokeGrant(ctx *context.Context) {
|
||||
if err := auth.RevokeOAuth2Grant(ctx, ctx.PathParamInt64("grantId"), oa.OwnerID); err != nil {
|
||||
grant, err := auth.GetOAuth2GrantByID(ctx, ctx.PathParamInt64("grantId"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOAuth2GrantByID", err)
|
||||
return
|
||||
}
|
||||
// grants belong to individual users, so this also rejects the instance-wide
|
||||
// (owner nil, ID 0) and organization handlers without assuming who routes here
|
||||
if grant == nil || oa.Owner == nil || grant.UserID != oa.Owner.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetOAuth2ApplicationByID", auth.IsErrOAuthApplicationNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.RevokeOAuth2Grant(ctx, grant.ID, oa.ownerID()); err != nil {
|
||||
ctx.ServerError("RevokeOAuth2Grant", err)
|
||||
return
|
||||
}
|
||||
|
||||
oa.recordAudit(ctx, audit.OAuth2ApplicationRevoke, app.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
|
||||
ctx.JSONRedirect(oa.BasePathList)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
@@ -57,6 +59,8 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserTwoFactorRegenerate, ctx.Doer, "two_factor_id", t.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
||||
}
|
||||
@@ -93,6 +97,8 @@ func DisableTwoFactor(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserTwoFactorDisable, ctx.Doer, "two_factor_id", t.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
||||
}
|
||||
@@ -273,6 +279,8 @@ func EnrollTwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserTwoFactorEnable, ctx.Doer)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/openid"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
@@ -105,6 +107,9 @@ func settingsOpenIDVerify(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
log.Trace("Associated OpenID %s to user %s", id, ctx.Doer.Name)
|
||||
|
||||
audit.Record(ctx, audit_model.UserOpenIDAdd, ctx.Doer, "openid", oid.URI)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
||||
@@ -117,7 +122,17 @@ func DeleteOpenID(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
|
||||
oid, err := user_model.GetUserOpenIDByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
} else {
|
||||
ctx.ServerError("GetUserOpenIDByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.DeleteUserOpenID(ctx, oid); err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
} else {
|
||||
@@ -125,6 +140,9 @@ func DeleteOpenID(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserOpenIDRemove, ctx.Doer, "openid", oid.URI)
|
||||
|
||||
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
@@ -58,6 +60,8 @@ func DeleteAccountLink(ctx *context.Context) {
|
||||
if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, id); err != nil {
|
||||
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserExternalLoginRemove, ctx.Doer, "auth_source_id", id)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
wa "gitea.dev/modules/auth/webauthn"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
@@ -124,13 +126,16 @@ func WebauthnRegisterPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Create the credential
|
||||
_, err = auth.CreateCredential(ctx, ctx.Doer.ID, name, cred)
|
||||
dbCred, err = auth.CreateCredential(ctx, ctx.Doer.ID, name, cred)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateCredential", err)
|
||||
return
|
||||
}
|
||||
_ = ctx.Session.Delete("webauthnName")
|
||||
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
||||
|
||||
audit.Record(ctx, audit_model.UserWebAuthAdd, ctx.Doer, "credential", dbCred.Name)
|
||||
|
||||
ctx.JSON(http.StatusCreated, cred)
|
||||
}
|
||||
|
||||
@@ -141,9 +146,17 @@ func WebauthnDelete(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := auth.DeleteCredential(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("GetWebAuthnCredentialByID", err)
|
||||
cred, err := auth.GetWebAuthnCredentialByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetWebAuthnCredentialByID", auth.IsErrWebAuthnCredentialNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := auth.DeleteCredential(ctx, cred.ID, ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("DeleteCredential", err)
|
||||
return
|
||||
} else if ok {
|
||||
audit.Record(ctx, audit_model.UserWebAuthRemove, ctx.Doer, "credential", cred.Name)
|
||||
}
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
@@ -37,9 +38,14 @@ func Webhooks(ctx *context.Context) {
|
||||
|
||||
// DeleteWebhook response for delete webhook
|
||||
func DeleteWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Doer.ID, ctx.FormInt64("id")); err != nil {
|
||||
hook, err := webhook.GetWebhookByOwnerID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetWebhookByOwnerID: " + err.Error())
|
||||
} else if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Doer.ID, hook.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error())
|
||||
} else {
|
||||
audit.RecordScoped(ctx, ctx.Doer, nil, audit.WebhookRemove, "webhook", hook.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"gitea.dev/routers/web/healthcheck"
|
||||
"gitea.dev/routers/web/misc"
|
||||
"gitea.dev/routers/web/org"
|
||||
org_setting "gitea.dev/routers/web/org/setting"
|
||||
"gitea.dev/routers/web/repo"
|
||||
"gitea.dev/routers/web/repo/actions"
|
||||
repo_setting "gitea.dev/routers/web/repo/setting"
|
||||
@@ -305,7 +307,7 @@ func Routes() *web.Router {
|
||||
routes.Get("/ssh_info", misc.SSHInfo)
|
||||
routes.Get("/api/healthz", healthcheck.Check)
|
||||
|
||||
mid = append(mid, common.MustInitSessioner(), context.Contexter())
|
||||
mid = append(mid, common.MustInitSessioner(), context.Contexter(), common.AuditOrigin(audit_model.OriginUI))
|
||||
|
||||
// Get user from session if logged in.
|
||||
webAuth := newWebAuthMiddleware()
|
||||
@@ -748,6 +750,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
addWebhookEditRoutes()
|
||||
}, webhooksEnabled)
|
||||
|
||||
m.Get("/audit_logs", user_setting.ViewAuditLogs)
|
||||
|
||||
m.Group("/blocked_users", func() {
|
||||
m.Get("", user_setting.BlockedUsers)
|
||||
m.Post("", web.Bind[*forms.BlockUserForm](), user_setting.BlockedUsersPost)
|
||||
@@ -795,6 +799,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
m.Get("/audit_logs", admin.ViewAuditLogs)
|
||||
m.Get("/audit_logs/export", admin.ExportAuditLogs)
|
||||
m.Get("/stats", admin.MonitorStats)
|
||||
m.Get("/cron", admin.CronTasks)
|
||||
m.Get("/perftrace", admin.PerfTrace)
|
||||
@@ -1056,6 +1062,8 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
addSettingsScopedWorkflowsRoutes()
|
||||
}, actions.MustEnableActions)
|
||||
|
||||
m.Get("/audit_logs", org_setting.ViewAuditLogs)
|
||||
|
||||
m.Post("/rename", web.Bind[*forms.RenameOrgForm](), org.SettingsRenamePost)
|
||||
m.Post("/delete", org.SettingsDeleteOrgPost)
|
||||
m.Post("/visibility", org.SettingsChangeVisibilityPost)
|
||||
@@ -1267,6 +1275,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/token_permissions", repo_setting.UpdateTokenPermissions)
|
||||
})
|
||||
}, actions.MustEnableActions)
|
||||
m.Get("/audit_logs", repo_setting.ViewAuditLogs)
|
||||
// the follow handler must be under "settings", otherwise this incomplete repo can't be accessed
|
||||
m.Group("/migrate", func() {
|
||||
m.Post("/retry", repo.MigrateRetryPost)
|
||||
|
||||
Reference in New Issue
Block a user