This commit is contained in:
KN4CK3R 2024-05-17 11:48:58 -05:00 committed by GitHub
commit 4f537cf704
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
145 changed files with 3631 additions and 461 deletions

View File

@ -4,6 +4,7 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
@ -11,6 +12,8 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/audit"
auth_service "code.gitea.io/gitea/services/auth"
"github.com/urfave/cli/v2"
@ -90,6 +93,26 @@ func runListAuth(c *cli.Context) error {
return nil
}
func createSource(ctx context.Context, source *auth_model.Source) error {
if err := auth_model.CreateSource(ctx, source); err != nil {
return err
}
audit.RecordSystemAuthenticationSourceAdd(ctx, user_model.NewCLIUser(), source)
return nil
}
func updateSource(ctx context.Context, source *auth_model.Source) error {
if err := auth_model.UpdateSource(ctx, source); err != nil {
return err
}
audit.RecordSystemAuthenticationSourceUpdate(ctx, user_model.NewCLIUser(), source)
return nil
}
func runDeleteAuth(c *cli.Context) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
@ -107,5 +130,5 @@ func runDeleteAuth(c *cli.Context) error {
return err
}
return auth_service.DeleteSource(ctx, source)
return auth_service.DeleteSource(ctx, user_model.NewCLIUser(), source)
}

View File

@ -9,6 +9,8 @@ import (
"strings"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/auth/source/ldap"
"github.com/urfave/cli/v2"
@ -308,58 +310,26 @@ func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authTyp
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
func (a *authService) addLdapBindDn(c *cli.Context) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
return err
}
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil {
return err
}
authSource := &auth.Source{
Type: auth.LDAP,
IsActive: true, // active by default
Cfg: &ldap.Source{
Enabled: true, // always true
},
}
parseAuthSource(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
return a.createAuthSource(ctx, authSource)
return a.addLdapSource(c, auth.LDAP, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute")
}
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
func (a *authService) updateLdapBindDn(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil {
return err
}
authSource, err := a.getAuthSource(ctx, c, auth.LDAP)
if err != nil {
return err
}
parseAuthSource(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
return a.updateAuthSource(ctx, authSource)
return a.updateLdapSource(c, auth.LDAP)
}
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
return a.addLdapSource(c, auth.DLDAP, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute")
}
// updateLdapBindDn updates a new LDAP (simple auth) authentication source.
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
return a.updateLdapSource(c, auth.DLDAP)
}
func (a *authService) addLdapSource(c *cli.Context, authType auth.Type, args ...string) error {
if err := argsSet(c, args...); err != nil {
return err
}
@ -369,9 +339,12 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
if err := a.initDB(ctx); err != nil {
return err
}
if err := audit.Init(); err != nil {
return err
}
authSource := &auth.Source{
Type: auth.DLDAP,
Type: authType,
IsActive: true, // active by default
Cfg: &ldap.Source{
Enabled: true, // always true
@ -383,19 +356,27 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
return err
}
return a.createAuthSource(ctx, authSource)
if err := a.createAuthSource(ctx, authSource); err != nil {
return err
}
audit.RecordSystemAuthenticationSourceAdd(ctx, user_model.NewCLIUser(), authSource)
return nil
}
// updateLdapBindDn updates a new LDAP (simple auth) authentication source.
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
func (a *authService) updateLdapSource(c *cli.Context, authType auth.Type) error {
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil {
return err
}
if err := audit.Init(); err != nil {
return err
}
authSource, err := a.getAuthSource(ctx, c, auth.DLDAP)
authSource, err := a.getAuthSource(ctx, c, authType)
if err != nil {
return err
}
@ -405,5 +386,11 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
return err
}
return a.updateAuthSource(ctx, authSource)
if err := a.updateAuthSource(ctx, authSource); err != nil {
return err
}
audit.RecordSystemAuthenticationSourceUpdate(ctx, user_model.NewCLIUser(), authSource)
return nil
}

View File

@ -184,7 +184,7 @@ func runAddOauth(c *cli.Context) error {
}
}
return auth_model.CreateSource(ctx, &auth_model.Source{
return createSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
@ -295,5 +295,5 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
return auth_model.UpdateSource(ctx, source)
return updateSource(ctx, source)
}

View File

@ -155,7 +155,7 @@ func runAddSMTP(c *cli.Context) error {
smtpConfig.Auth = "PLAIN"
}
return auth_model.CreateSource(ctx, &auth_model.Source{
return createSource(ctx, &auth_model.Source{
Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
@ -196,5 +196,5 @@ func runUpdateSMTP(c *cli.Context) error {
source.Cfg = smtpConfig
return auth_model.UpdateSource(ctx, source)
return updateSource(ctx, source)
}

View File

@ -62,7 +62,7 @@ func runChangePassword(c *cli.Context) error {
Password: optional.Some(c.String("password")),
MustChangePassword: optional.Some(c.Bool("must-change-password")),
}
if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
if err := user_service.UpdateAuth(ctx, user_model.NewCLIUser(), user, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
return fmt.Errorf("password is not long enough, needs to be at least %d characters", setting.MinPasswordLength)

View File

@ -14,6 +14,7 @@ import (
pwd "code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"github.com/urfave/cli/v2"
)
@ -103,6 +104,9 @@ func runCreateUser(c *cli.Context) error {
return err
}
}
if err := audit.Init(); err != nil {
return err
}
var password string
if c.IsSet("password") {
@ -162,6 +166,8 @@ func runCreateUser(c *cli.Context) error {
return fmt.Errorf("CreateUser: %w", err)
}
audit.RecordUserCreate(ctx, user_model.NewCLIUser(), u)
if c.Bool("access-token") {
t := &auth_model.AccessToken{
Name: "gitea-admin",
@ -172,6 +178,8 @@ func runCreateUser(c *cli.Context) error {
return err
}
audit.RecordUserAccessTokenAdd(ctx, user_model.NewCLIUser(), u, t)
fmt.Printf("Access token was successfully created... %s\n", t.Token)
}

View File

@ -10,6 +10,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/services/audit"
user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v2"
@ -52,7 +53,9 @@ func runDeleteUser(c *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
if err := audit.Init(); err != nil {
return err
}
if err := storage.Init(); err != nil {
return err
}
@ -77,5 +80,5 @@ func runDeleteUser(c *cli.Context) error {
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
}
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
return user_service.DeleteUser(ctx, user_model.NewCLIUser(), user, c.Bool("purge"))
}

View File

@ -9,6 +9,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/audit"
"github.com/urfave/cli/v2"
)
@ -52,6 +53,9 @@ func runGenerateAccessToken(c *cli.Context) error {
if err := initDB(ctx); err != nil {
return err
}
if err := audit.Init(); err != nil {
return err
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
@ -84,6 +88,8 @@ func runGenerateAccessToken(c *cli.Context) error {
return err
}
audit.RecordUserAccessTokenAdd(ctx, user_model.NewCLIUser(), user, t)
if c.Bool("raw") {
fmt.Printf("%s\n", t.Token)
} else {

View File

@ -15,6 +15,8 @@ import (
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
@ -23,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install"
"code.gitea.io/gitea/services/audit"
"github.com/felixge/fgprof"
"github.com/urfave/cli/v2"
@ -209,7 +212,13 @@ func serveInstalled(ctx *cli.Context) error {
// Set up Chi routes
webRoutes := routers.NormalRoutes()
audit.RecordSystemStartup(db.DefaultContext, user_model.NewCLIUser(), setting.AppVer)
err := listen(webRoutes, true)
audit.RecordSystemShutdown(db.DefaultContext, user_model.NewCLIUser())
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.GetManager().Close()

View File

@ -659,6 +659,32 @@ LEVEL = Info
;; Host address
;ADDR =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[audit]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Enable logging of audit events
;ENABLED = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[audit.file]
;; Set the file name for the logger. If this is a relative path this
;; will be relative to log.ROOT_PATH
;; Defaults to log.ROOT_PATH/audit.log
;FILE_NAME =
;; This enables automated audit log rotate, default is true
;LOG_ROTATE = true
;; Maximum file size in bytes before rotating takes place (format `1000`, `1 MB`, `1 GiB`)
;MAXIMUM_SIZE = 256 MB
;; Rotate audit log daily, default is true
;DAILY_ROTATE = true
;; Delete the audit log file after n days, default is 7
;MAX_DAYS = 7
;; Compress audit logs with gzip
;COMPRESS = true
;; Compression level see godoc for compress/gzip
;COMPRESSION_LEVEL = -1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[git]

View File

@ -0,0 +1,168 @@
---
date: "2023-04-21T00:00:00+00:00"
title: "Audit Logging"
slug: "audit-logging"
sidebar_position: 43
toc: false
draft: false
menu:
sidebar:
parent: "administration"
name: "Audit Logging"
sidebar_position: 43
identifier: "audit-logging"
---
# Audit Logging
Audit logging is used to track security related events and provide documentary evidence of the sequence of important activities.
**Table of Contents**
{{< toc >}}
## Appenders
The audit log supports different appenders:
- `log`: Log events as information to the configured Gitea logging
- `file`: Write events as JSON objects to a file
The config documentation lists all available options to configure audit logging with appenders.
## Events
Audit events are grouped by `user`, `organization`, `repository` and `system`.
### User Events
| Event | Description |
| - | - |
| `user:impersonation` | Admin impersonating user |
| `user:create` | Created user |
| `user:delete` | Deleted user |
| `user:authentication:fail:twofactor` | Failed two-factor authentication for user |
| `user:authentication:source` | Changed authentication source of user |
| `user:active` | Changed activation status of user |
| `user:restricted` | Changed restriction status of user |
| `user:admin` | Changed admin status of user |
| `user:name` | Changed user name |
| `user:password` | Changed password of user |
| `user:password:resetrequest` | Requested a password reset |
| `user:visibility` | Changed visibility of user |
| `user:email:primary` | Changed primary email of user |
| `user:email:add` | Added email to user |
| `user:email:activate` | Activated email of user |
| `user:email:remove` | Removed email from user |
| `user:twofactor:enable` | User enabled two-factor authentication |
| `user:twofactor:regenerate` | User regenerated two-factor authentication secret |
| `user:twofactor:disable` | User disabled two-factor authentication |
| `user:webauth:add` | User added WebAuthn key |
| `user:webauth:remove` | User removed WebAuthn key |
| `user:externallogin:add` | Added external login for user |
| `user:externallogin:remove` | Removed external login for user |
| `user:openid:add` | Associated OpenID to user |
| `user:openid:remove` | Removed OpenID from user |
| `user:accesstoken:add` | Added access token for user |
| `user:accesstoken:remove` | Removed access token from user |
| `user:oauth2application:add` | Created OAuth2 application |
| `user:oauth2application:update` | Updated OAuth2 application |
| `user:oauth2application:secret` | Regenerated secret for OAuth2 application |
| `user:oauth2application:grant` | Granted OAuth2 access to application |
| `user:oauth2application:revoke` | Revoked OAuth2 grant for application |
| `user:oauth2application:remove` | Removed OAuth2 application |
| `user:key:ssh:add` | Added SSH key |
| `user:key:ssh:remove` | Removed SSH key |
| `user:key:principal:add` | Added principal key |
| `user:key:principal:remove` | Removed principal key |
| `user:key:gpg:add` | Added GPG key |
| `user:key:gpg:remove` | Added GPG key |
| `user:secret:add` | Added secret |
| `user:secret:update` | Updated secret |
| `user:secret:remove` | Removed secret |
| `user:webhook:add` | Added webhook |
| `user:webhook:update` | Updated webhook |
| `user:webhook:remove` | Removed webhook |
### Organization Events
| Event | Description |
| - | - |
| `organization:create` | Created organization |
| `organization:delete` | Deleted organization |
| `organization:name` | Changed organization name |
| `organization:visibility` | Changed visibility of organization |
| `organization:team:add` | Added team to organization |
| `organization:team:update` | Updated settings of team |
| `organization:team:remove` | Removed team from organization |
| `organization:team:permission` | Changed permission of team |
| `organization:team:member:add` | Added user to team |
| `organization:team:member:remove` | Removed User from team |
| `organization:oauth2application:add` | Created OAuth2 application |
| `organization:oauth2application:update` | Updated OAuth2 application |
| `organization:oauth2application:secret` | Regenerated secret for OAuth2 application |
| `organization:oauth2application:remove` | Removed OAuth2 application |
| `organization:secret:add` | Added secret |
| `organization:secret:update` | Updated secret |
| `organization:secret:remove` | Removed secret |
| `organization:webhook:add` | Added webhook |
| `organization:webhook:update` | Updated webhook |
| `organization:webhook:remove` | Removed webhook |
### Repository Events
| Event | Description |
| - | - |
| `repository:create` | Crated repository |
| `repository:create:fork` | Created fork of repository |
| `repository:archive` | Archived repository |
| `repository:unarchive` | Unarchived repository |
| `repository:delete` | Deleted repository |
| `repository:name` | Changed repository name |
| `repository:visibility` | Changed visibility of repository |
| `repository:convert:fork` | Converted repository from fork to regular repository |
| `repository:convert:mirror` | Converted repository from mirror to regular repository |
| `repository:mirror:push:add` | Added push mirror for repository |
| `repository:mirror:push:remove` | Removed push mirror from repository |
| `repository:signingverification` | Changed signing verification of repository |
| `repository:transfer:start` | Started repository transfer |
| `repository:transfer:finish` | Transferred repository to new owner |
| `repository:transfer:cancel` | Canceled repository transfer |
| `repository:wiki:delete` | Deleted wiki of repository |
| `repository:collaborator:add` | Added user as collaborator for repository |
| `repository:collaborator:access` | Changed access mode of collaborator |
| `repository:collaborator:remove` | Removed user as collaborator of repository |
| `repository:collaborator:team:add` | Added team as collaborator for repository |
| `repository:collaborator:team:remove` | Removed team as collaborator of repository |
| `repository:branch:default` | Changed default branch |
| `repository:branch:protection:add` | Added branch protection |
| `repository:branch:protection:update` | Updated branch protection |
| `repository:branch:protection:remove` | Removed branch protection |
| `repository:tag:protection:add` | Added tag protection |
| `repository:tag:protection:update` | Updated tag protection |
| `repository:tag:protection:remove` | Removed tag protection |
| `repository:webhook:add` | Added webhook |
| `repository:webhook:update` | Updated webhook |
| `repository:webhook:remove` | Removed webhook |
| `repository:deploykey:add` | Added deploy key |
| `repository:deploykey:remove` | Removed deploy key |
| `repository:secret:add` | Added secret |
| `repository:secret:update` | Updated secret |
| `repository:secret:remove` | Removed secret |
### System Events
| Event | Description |
| - | - |
| `system:startup` | System startup |
| `system:shutdown` | Normal system shutdown (unexpected shutdowns may not be logged) |
| `system:webhook:add` | Added webhook |
| `system:webhook:update` | Updated webhook |
| `system:webhook:remove` | Removed webhook |
| `system:authenticationsource:add` | Created authentication source |
| `system:authenticationsource:update` | Updated authentication source |
| `system:authenticationsource:remove` | Removed authentication source |
| `system:oauth2application:add` | Created OAuth2 application |
| `system:oauth2application:update` | Updated OAuth2 application |
| `system:oauth2application:secret` | Regenerated secret for OAuth2 application |
| `system:oauth2application:remove` | Removed OAuth2 application |

View File

@ -912,6 +912,21 @@ Default templates for project boards:
- `PROTOCOL`: **tcp**: Set the protocol, either "tcp", "unix" or "udp".
- `ADDR`: **:7020**: Sets the address to connect to.
## Audit Log (`audit`)
- `ENABLED`: **false**: Enable logging of audit events
## File Audit Log (`audit.file`)
- `ENABLED`: **false**: Enable logging of audit events to file
- `FILENAME`: **\<empty\>**: Set the file name for the logger. If this is a relative path this will be relative to `log.ROOT_PATH`. Defaults to `log.ROOT_PATH/audit.log`.
- `ROTATE`: **true**: This enables automated audit log rotate, default is true
- `MAXIMUM_SIZE`: **256 MB**: Maximum file size in bytes before rotating takes place (format `1000`, `1 MB`, `1 GiB`)
- `ROTATE_DAILY`: **true**: Rotate audit log daily, default is true
- `KEEP_DAYS`: **7**: Delete the audit log file after n days, default is 7
- `COMPRESS`: **true**: Compress audit logs with gzip
- `COMPRESSION_LEVEL`: **-1**: Compression level see godoc for `compress/gzip`
## Cron (`cron`)
- `ENABLED`: **false**: Enable to run all cron tasks periodically with default settings.

View File

@ -295,17 +295,17 @@ func PublicKeyIsExternallyManaged(ctx context.Context, id int64) (bool, error) {
return false, nil
}
// deleteKeysMarkedForDeletion returns true if ssh keys needs update
func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, error) {
// deleteKeysMarkedForDeletion returns the deleted keys
func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) ([]*PublicKey, error) {
// Start session
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return false, err
return nil, err
}
defer committer.Close()
// Delete keys marked for deletion
var sshKeysNeedUpdate bool
deletedKeys := make([]*PublicKey, 0, len(keys))
for _, KeyToDelete := range keys {
key, err := SearchPublicKeyByContent(ctx, KeyToDelete)
if err != nil {
@ -316,19 +316,21 @@ func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, erro
log.Error("DeleteByID[PublicKey]: %v", err)
continue
}
sshKeysNeedUpdate = true
deletedKeys = append(deletedKeys, key)
}
if err := committer.Commit(); err != nil {
return false, err
return nil, err
}
return sshKeysNeedUpdate, nil
return deletedKeys, nil
}
// AddPublicKeysBySource add a users public keys. Returns true if there are changes.
func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
var sshKeysNeedUpdate bool
// AddPublicKeysBySource add a users public keys. Returns the added keys.
func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) []*PublicKey {
addedKeys := make([]*PublicKey, 0, len(sshPublicKeys))
for _, sshKey := range sshPublicKeys {
var err error
found := false
@ -346,28 +348,27 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So
marshalled = marshalled[:len(marshalled)-1]
sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out))
if _, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID); err != nil {
if pubKey, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID); err != nil {
if IsErrKeyAlreadyExist(err) {
log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name)
} else {
log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
}
} else {
addedKeys = append(addedKeys, pubKey)
log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
sshKeysNeedUpdate = true
}
}
if !found && err != nil {
log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
}
}
return sshKeysNeedUpdate
return addedKeys
}
// SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
var sshKeysNeedUpdate bool
// SynchronizePublicKeys updates a users public keys. Returns the updated keys.
func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) (addedKeys, deletedKeys []*PublicKey) {
log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
// Get Public Keys from DB with current LDAP source
@ -399,7 +400,7 @@ func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.So
// Check if Public Key sync is needed
if util.SliceSortedEqual(giteaKeys, providedKeys) {
log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
return false
return nil, nil
}
log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
@ -410,9 +411,8 @@ func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.So
newKeys = append(newKeys, key)
}
}
if AddPublicKeysBySource(ctx, usr, s, newKeys) {
sshKeysNeedUpdate = true
}
addedKeys = AddPublicKeysBySource(ctx, usr, s, newKeys)
// Mark keys from DB that no longer exist in the source for deletion
var giteaKeysToDelete []string
@ -424,13 +424,10 @@ func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.So
}
// Delete keys from DB that no longer exist in the source
needUpd, err := deleteKeysMarkedForDeletion(ctx, giteaKeysToDelete)
deletedKeys, err = deleteKeysMarkedForDeletion(ctx, giteaKeysToDelete)
if err != nil {
log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
}
if needUpd {
sshKeysNeedUpdate = true
}
return sshKeysNeedUpdate
return addedKeys, deletedKeys
}

125
models/audit/action.go Normal file
View File

@ -0,0 +1,125 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
type Action string
const (
UserImpersonation Action = "user:impersonation"
UserCreate Action = "user:create"
UserDelete Action = "user:delete"
UserAuthenticationFailTwoFactor Action = "user:authentication:fail:twofactor"
UserAuthenticationSource Action = "user:authentication:source"
UserActive Action = "user:active"
UserRestricted Action = "user:restricted"
UserAdmin Action = "user:admin"
UserName Action = "user:name"
UserPassword Action = "user:password"
UserPasswordResetRequest Action = "user:password:resetrequest"
UserVisibility Action = "user:visibility"
UserEmailPrimaryChange Action = "user:email:primary"
UserEmailAdd Action = "user:email:add"
UserEmailActivate Action = "user:email:activate"
UserEmailRemove Action = "user:email:remove"
UserTwoFactorEnable Action = "user:twofactor:enable"
UserTwoFactorRegenerate Action = "user:twofactor:regenerate"
UserTwoFactorDisable Action = "user:twofactor:disable"
UserWebAuthAdd Action = "user:webauth:add"
UserWebAuthRemove Action = "user:webauth:remove"
UserExternalLoginAdd Action = "user:externallogin:add"
UserExternalLoginRemove Action = "user:externallogin:remove"
UserOpenIDAdd Action = "user:openid:add"
UserOpenIDRemove Action = "user:openid:remove"
UserAccessTokenAdd Action = "user:accesstoken:add"
UserAccessTokenRemove Action = "user:accesstoken:remove"
UserOAuth2ApplicationAdd Action = "user:oauth2application:add"
UserOAuth2ApplicationUpdate Action = "user:oauth2application:update"
UserOAuth2ApplicationSecret Action = "user:oauth2application:secret"
UserOAuth2ApplicationGrant Action = "user:oauth2application:grant"
UserOAuth2ApplicationRevoke Action = "user:oauth2application:revoke"
UserOAuth2ApplicationRemove Action = "user:oauth2application:remove"
UserKeySSHAdd Action = "user:key:ssh:add"
UserKeySSHRemove Action = "user:key:ssh:remove"
UserKeyPrincipalAdd Action = "user:key:principal:add"
UserKeyPrincipalRemove Action = "user:key:principal:remove"
UserKeyGPGAdd Action = "user:key:gpg:add"
UserKeyGPGRemove Action = "user:key:gpg:remove"
UserSecretAdd Action = "user:secret:add"
UserSecretUpdate Action = "user:secret:update"
UserSecretRemove Action = "user:secret:remove"
UserWebhookAdd Action = "user:webhook:add"
UserWebhookUpdate Action = "user:webhook:update"
UserWebhookRemove Action = "user:webhook:remove"
OrganizationCreate Action = "organization:create"
OrganizationDelete Action = "organization:delete"
OrganizationName Action = "organization:name"
OrganizationVisibility Action = "organization:visibility"
OrganizationTeamAdd Action = "organization:team:add"
OrganizationTeamUpdate Action = "organization:team:update"
OrganizationTeamRemove Action = "organization:team:remove"
OrganizationTeamPermission Action = "organization:team:permission"
OrganizationTeamMemberAdd Action = "organization:team:member:add"
OrganizationTeamMemberRemove Action = "organization:team:member:remove"
OrganizationOAuth2ApplicationAdd Action = "organization:oauth2application:add"
OrganizationOAuth2ApplicationUpdate Action = "organization:oauth2application:update"
OrganizationOAuth2ApplicationSecret Action = "organization:oauth2application:secret"
OrganizationOAuth2ApplicationRemove Action = "organization:oauth2application:remove"
OrganizationSecretAdd Action = "organization:secret:add"
OrganizationSecretUpdate Action = "organization:secret:update"
OrganizationSecretRemove Action = "organization:secret:remove"
OrganizationWebhookAdd Action = "organization:webhook:add"
OrganizationWebhookUpdate Action = "organization:webhook:update"
OrganizationWebhookRemove Action = "organization:webhook:remove"
RepositoryCreate Action = "repository:create"
RepositoryCreateFork Action = "repository:create:fork"
RepositoryArchive Action = "repository:archive"
RepositoryUnarchive Action = "repository:unarchive"
RepositoryDelete Action = "repository:delete"
RepositoryName Action = "repository:name"
RepositoryVisibility Action = "repository:visibility"
RepositoryConvertFork Action = "repository:convert:fork"
RepositoryConvertMirror Action = "repository:convert:mirror"
RepositoryMirrorPushAdd Action = "repository:mirror:push:add"
RepositoryMirrorPushRemove Action = "repository:mirror:push:remove"
RepositorySigningVerification Action = "repository:signingverification"
RepositoryTransferStart Action = "repository:transfer:start"
RepositoryTransferFinish Action = "repository:transfer:finish"
RepositoryTransferCancel Action = "repository:transfer:cancel"
RepositoryWikiDelete Action = "repository:wiki:delete"
RepositoryCollaboratorAdd Action = "repository:collaborator:add"
RepositoryCollaboratorAccess Action = "repository:collaborator:access"
RepositoryCollaboratorRemove Action = "repository:collaborator:remove"
RepositoryCollaboratorTeamAdd Action = "repository:collaborator:team:add"
RepositoryCollaboratorTeamRemove Action = "repository:collaborator:team:remove"
RepositoryBranchDefault Action = "repository:branch:default"
RepositoryBranchProtectionAdd Action = "repository:branch:protection:add"
RepositoryBranchProtectionUpdate Action = "repository:branch:protection:update"
RepositoryBranchProtectionRemove Action = "repository:branch:protection:remove"
RepositoryTagProtectionAdd Action = "repository:tag:protection:add"
RepositoryTagProtectionUpdate Action = "repository:tag:protection:update"
RepositoryTagProtectionRemove Action = "repository:tag:protection:remove"
RepositoryWebhookAdd Action = "repository:webhook:add"
RepositoryWebhookUpdate Action = "repository:webhook:update"
RepositoryWebhookRemove Action = "repository:webhook:remove"
RepositoryDeployKeyAdd Action = "repository:deploykey:add"
RepositoryDeployKeyRemove Action = "repository:deploykey:remove"
RepositorySecretAdd Action = "repository:secret:add"
RepositorySecretUpdate Action = "repository:secret:update"
RepositorySecretRemove Action = "repository:secret:remove"
SystemStartup Action = "system:startup"
SystemShutdown Action = "system:shutdown"
SystemWebhookAdd Action = "system:webhook:add"
SystemWebhookUpdate Action = "system:webhook:update"
SystemWebhookRemove Action = "system:webhook:remove"
SystemAuthenticationSourceAdd Action = "system:authenticationsource:add"
SystemAuthenticationSourceUpdate Action = "system:authenticationsource:update"
SystemAuthenticationSourceRemove Action = "system:authenticationsource:remove"
SystemOAuth2ApplicationAdd Action = "system:oauth2application:add"
SystemOAuth2ApplicationUpdate Action = "system:oauth2application:update"
SystemOAuth2ApplicationSecret Action = "system:oauth2application:secret"
SystemOAuth2ApplicationRemove Action = "system:oauth2application:remove"
)

101
models/audit/audit_event.go Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)
func init() {
db.RegisterModel(new(Event))
}
type Event struct {
ID int64 `xorm:"pk autoincr"`
Action Action `xorm:"INDEX NOT NULL"`
ActorID int64 `xorm:"INDEX NOT NULL"`
ScopeType ObjectType `xorm:"INDEX(scope) NOT NULL"`
ScopeID int64 `xorm:"INDEX(scope) NOT NULL"`
TargetType ObjectType `xorm:"NOT NULL"`
TargetID int64 `xorm:"NOT NULL"`
Message string
IPAddress string
TimestampUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL"`
}
func (*Event) TableName() string {
return "audit_event"
}
func InsertEvent(ctx context.Context, e *Event) (*Event, error) {
return e, db.Insert(ctx, e)
}
type EventSort = string
const (
SortTimestampAsc EventSort = "timestamp_asc"
SortTimestampDesc EventSort = "timestamp_desc"
)
type EventSearchOptions struct {
Action Action
ActorID int64
ScopeType ObjectType
ScopeID int64
Sort EventSort
db.Paginator
}
func (opts *EventSearchOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.Action != "" {
cond = cond.And(builder.Eq{"action": opts.Action})
}
if opts.ActorID != 0 {
cond = cond.And(builder.Eq{"actor_id": opts.ActorID})
}
if opts.ScopeID != 0 && opts.ScopeType != "" {
cond = cond.And(builder.Eq{
"audit_event.scope_type": opts.ScopeType,
"audit_event.scope_id": opts.ScopeID,
})
}
return cond
}
func (opts *EventSearchOptions) configureOrderBy(e db.Engine) {
switch opts.Sort {
case SortTimestampAsc:
e.Asc("timestamp_unix")
default:
e.Desc("timestamp_unix")
}
// Sort by id for stable order with duplicates in the other field
e.Asc("id")
}
func FindEvents(ctx context.Context, opts *EventSearchOptions) ([]*Event, int64, error) {
sess := db.GetEngine(ctx).
Where(opts.ToConds()).
Table("audit_event")
opts.configureOrderBy(sess)
if opts.Paginator != nil {
sess = db.SetSessionPagination(sess, opts)
}
evs := make([]*Event, 0, 10)
count, err := sess.FindAndCount(&evs)
return evs, count, err
}

30
models/audit/types.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
type ObjectType string
const (
TypeSystem ObjectType = "system"
TypeRepository ObjectType = "repository"
TypeUser ObjectType = "user"
TypeOrganization ObjectType = "organization"
TypeEmailAddress ObjectType = "email_address"
TypeTeam ObjectType = "team"
TypeTwoFactor ObjectType = "twofactor"
TypeWebAuthnCredential ObjectType = "webauthn"
TypeOpenID ObjectType = "openid"
TypeAccessToken ObjectType = "access_token"
TypeOAuth2Application ObjectType = "oauth2_application"
TypeOAuth2Grant ObjectType = "oauth2_grant"
TypeAuthenticationSource ObjectType = "authentication_source"
TypePublicKey ObjectType = "public_key"
TypeGPGKey ObjectType = "gpg_key"
TypeSecret ObjectType = "secret"
TypeWebhook ObjectType = "webhook"
TypeProtectedTag ObjectType = "protected_tag"
TypeProtectedBranch ObjectType = "protected_branch"
TypePushMirror ObjectType = "push_mirror"
TypeRepoTransfer ObjectType = "repo_transfer"
)

View File

@ -222,6 +222,19 @@ func UpdateAccessToken(ctx context.Context, t *AccessToken) error {
return err
}
func GetAccessTokenByID(ctx context.Context, id, userID int64) (*AccessToken, error) {
t := &AccessToken{
UID: userID,
}
has, err := db.GetEngine(ctx).ID(id).Get(t)
if err != nil {
return nil, err
} else if !has {
return nil, ErrAccessTokenNotExist{}
}
return t, nil
}
// DeleteAccessTokenByID deletes access token by given ID.
func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error {
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_22"
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -587,6 +588,9 @@ var migrations = []Migration{
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// Gitea 1.22.0-rc1 ends at 299
// v299 -> v300
NewMigration("Add audit_event table", v1_23.AddAuditEventTable),
}
// GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,27 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddAuditEventTable(x *xorm.Engine) error {
type AuditEvent struct {
ID int64 `xorm:"pk autoincr"`
Action string `xorm:"INDEX NOT NULL"`
ActorID int64 `xorm:"INDEX NOT NULL"`
ScopeType string `xorm:"INDEX(scope) NOT NULL"`
ScopeID int64 `xorm:"INDEX(scope) NOT NULL"`
TargetType string `xorm:"NOT NULL"`
TargetID int64 `xorm:"NOT NULL"`
Message string
IPAddress string
TimestampUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL"`
}
return x.Sync(&AuditEvent{})
}

View File

@ -55,36 +55,39 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R
// addAllRepositories adds all repositories to the team.
// If the team already has some repositories they will be left unchanged.
func addAllRepositories(ctx context.Context, t *organization.Team) error {
func addAllRepositories(ctx context.Context, t *organization.Team) ([]*repo_model.Repository, error) {
orgRepos, err := organization.GetOrgRepositories(ctx, t.OrgID)
if err != nil {
return fmt.Errorf("get org repos: %w", err)
return nil, fmt.Errorf("get org repos: %w", err)
}
added := make([]*repo_model.Repository, 0, len(orgRepos))
for _, repo := range orgRepos {
if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
if err := AddRepository(ctx, t, repo); err != nil {
return fmt.Errorf("AddRepository: %w", err)
return nil, fmt.Errorf("AddRepository: %w", err)
}
added = append(added, repo)
}
}
return nil
return added, nil
}
// AddAllRepositories adds all repositories to the team
func AddAllRepositories(ctx context.Context, t *organization.Team) (err error) {
func AddAllRepositories(ctx context.Context, t *organization.Team) ([]*repo_model.Repository, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
return nil, err
}
defer committer.Close()
if err = addAllRepositories(ctx, t); err != nil {
return err
added, err := addAllRepositories(ctx, t)
if err != nil {
return nil, err
}
return committer.Commit()
return added, committer.Commit()
}
// RemoveAllRepositories removes all repositories from team and recalculates access
@ -204,7 +207,7 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
// Add all repositories to the team if it has access to all of them.
if t.IncludesAllRepositories {
err = addAllRepositories(ctx, t)
_, err = addAllRepositories(ctx, t)
if err != nil {
return fmt.Errorf("addAllRepositories: %w", err)
}
@ -282,7 +285,7 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
// Add all repositories to the team if it has access to all of them.
if includeAllChanged && t.IncludesAllRepositories {
err = addAllRepositories(ctx, t)
_, err = addAllRepositories(ctx, t)
if err != nil {
return fmt.Errorf("addAllRepositories: %w", err)
}

View File

@ -91,6 +91,11 @@ func OrgFromUser(user *user_model.User) *Organization {
return (*Organization)(user)
}
// UserFromOrg converts organization to user
func UserFromOrg(org *Organization) *user_model.User {
return (*user_model.User)(org)
}
// TableName represents the real table name of Organization
func (Organization) TableName() string {
return "user"

View File

@ -352,7 +352,7 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
}
// VerifyActiveEmailCode verifies active email code when active account
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
func VerifyActiveEmailCode(ctx context.Context, code, email string) (*User, *EmailAddress) {
minutes := setting.Service.ActiveCodeLives
if user := GetVerifyUser(ctx, code); user != nil {
@ -363,11 +363,11 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
if base.VerifyTimeLimitCode(data, minutes, prefix) {
emailAddress := &EmailAddress{UID: user.ID, Email: email}
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
return emailAddress
return user, emailAddress
}
}
}
return nil
return nil, nil
}
// SearchEmailOrderBy is used to sort the results from SearchEmails()
@ -453,10 +453,10 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
// ActivateUserEmail will change the activated state of an email address,
// either primary or secondary (all in the email_address table)
func ActivateUserEmail(ctx context.Context, userID int64, email string, activate bool) (err error) {
func ActivateUserEmail(ctx context.Context, userID int64, email string, activate bool) (*EmailAddress, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
return nil, err
}
defer committer.Close()
@ -464,48 +464,48 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
// First check if there's another user active with the same address
addr, exist, err := db.Get[EmailAddress](ctx, builder.Eq{"uid": userID, "lower_email": strings.ToLower(email)})
if err != nil {
return err
return nil, err
} else if !exist {
return fmt.Errorf("no such email: %d (%s)", userID, email)
return nil, fmt.Errorf("no such email: %d (%s)", userID, email)
}
if addr.IsActivated == activate {
// Already in the desired state; no action
return nil
return addr, nil
}
if activate {
if used, err := IsEmailActive(ctx, email, addr.ID); err != nil {
return fmt.Errorf("unable to check isEmailActive() for %s: %w", email, err)
return nil, fmt.Errorf("unable to check isEmailActive() for %s: %w", email, err)
} else if used {
return ErrEmailAlreadyUsed{Email: email}
return nil, ErrEmailAlreadyUsed{Email: email}
}
}
if err = updateActivation(ctx, addr, activate); err != nil {
return fmt.Errorf("unable to updateActivation() for %d:%s: %w", addr.ID, addr.Email, err)
return nil, fmt.Errorf("unable to updateActivation() for %d:%s: %w", addr.ID, addr.Email, err)
}
// Activate/deactivate a user's primary email address and account
if addr.IsPrimary {
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
if err != nil {
return err
return nil, err
} else if !exist {
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
return nil, fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
}
// The user's activation state should be synchronized with the primary email
if user.IsActive != activate {
user.IsActive = activate
if user.Rands, err = GetUserSalt(); err != nil {
return fmt.Errorf("unable to generate salt: %w", err)
return nil, fmt.Errorf("unable to generate salt: %w", err)
}
if err = UpdateUserCols(ctx, user, "is_active", "rands"); err != nil {
return fmt.Errorf("unable to updateUserCols() for user ID: %d: %w", userID, err)
return nil, fmt.Errorf("unable to updateUserCols() for user ID: %d: %w", userID, err)
}
}
}
return committer.Commit()
return addr, committer.Commit()
}
// validateEmailBasic checks whether the email complies with the rules

View File

@ -9,6 +9,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// ErrOpenIDNotExist openid is not known
@ -40,6 +42,21 @@ func GetUserOpenIDs(ctx context.Context, uid int64) ([]*UserOpenID, error) {
return openids, nil
}
func GetUserOpenID(ctx context.Context, id, uid int64) (*UserOpenID, error) {
openid, has, err := db.Get[UserOpenID](ctx, builder.Eq{
"id": id,
"uid": uid,
})
if err != nil {
return nil, err
} else if !has {
return nil, util.ErrNotExist
}
return openid, nil
}
// isOpenIDUsed returns true if the openid has been used.
func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
if len(uri) == 0 {

View File

@ -68,3 +68,19 @@ func NewActionsUser() *User {
func (u *User) IsActions() bool {
return u != nil && u.ID == ActionsUserID
}
func NewCLIUser() *User {
return &User{
ID: -3,
Name: "CLI",
LowerName: "cli",
}
}
func NewAuthenticationSourceUser() *User {
return &User{
ID: -4,
Name: "AuthenticationSource",
LowerName: "authenticationsource",
}
}

View File

@ -204,3 +204,14 @@ func TimeoutDialer(cTimeout time.Duration) func(ctx context.Context, net, addr s
func (r *Request) GoString() string {
return fmt.Sprintf("%s %s", r.req.Method, r.url)
}
func TryGetIPAddress(ctx context.Context) string {
if req, _ := ctx.Value(RequestContextKey).(*http.Request); req != nil {
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return req.RemoteAddr
}
return host
}
return ""
}

61
modules/setting/audit.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"compress/gzip"
"os"
"path"
"path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
var Audit = struct {
Enabled bool
FileOptions *log.WriterFileOption `ini:"-"`
}{
Enabled: false,
}
func loadAuditFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "audit", &Audit)
sec, err := rootCfg.GetSection("audit.file")
if err == nil {
if !ConfigSectionKeyBool(sec, "ENABLED") {
return
}
opts := &log.WriterFileOption{
FileName: path.Join(Log.RootPath, "audit.log"),
LogRotate: true,
DailyRotate: true,
MaxDays: 7,
Compress: true,
CompressionLevel: gzip.DefaultCompression,
}
if err := sec.MapTo(opts); err != nil {
log.Fatal("Failed to map audit file settings: %v", err)
}
opts.FileName = util.FilePathJoinAbs(opts.FileName)
if !filepath.IsAbs(opts.FileName) {
opts.FileName = path.Join(Log.RootPath, opts.FileName)
}
if err := os.MkdirAll(filepath.Dir(opts.FileName), os.ModePerm); err != nil {
log.Fatal("Unable to create directory for audit log %s: %v", opts.FileName, err)
}
opts.MaxSize = mustBytes(sec, "MAXIMUM_SIZE")
if opts.MaxSize <= 0 {
opts.MaxSize = 1 << 28
}
Audit.FileOptions = opts
}
}

View File

@ -153,6 +153,8 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadMirrorFrom(cfg)
loadMarkupFrom(cfg)
loadOtherFrom(cfg)
loadQueueFrom(cfg)
loadAuditFrom(cfg)
return nil
}
@ -217,7 +219,7 @@ func LoadSettings() {
loadMigrationsFrom(CfgProvider)
loadIndexerFrom(CfgProvider)
loadTaskFrom(CfgProvider)
LoadQueueSettings()
loadQueueFrom(CfgProvider)
loadProjectFrom(CfgProvider)
loadMimeTypeMapFrom(CfgProvider)
loadFederationFrom(CfgProvider)

View File

@ -3257,6 +3257,17 @@ config.xorm_log_sql = Log SQL
config.set_setting_failed = Set setting %s failed
monitor.audit.title = Audit Logs
monitor.audit.actor = Actor
monitor.audit.scope = Scope
monitor.audit.target = Target
monitor.audit.action = Action
monitor.audit.ip_address = IP Address
monitor.audit.timestamp = Timestamp
monitor.audit.no_events = There are no audit events matching the filter.
monitor.audit.deleted.actor = (removed)
monitor.audit.deleted.type = (removed %s [%v])
monitor.stats = Stats
monitor.cron = Cron Tasks

View File

@ -13,6 +13,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@ -74,6 +75,8 @@ func CreateOrg(ctx *context.APIContext) {
return
}
audit.RecordUserCreate(ctx, ctx.Doer, org.AsUser())
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
}

View File

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/mailer"
@ -152,6 +153,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.RecordUserCreate(ctx, ctx.Doer, u)
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
@ -199,7 +202,7 @@ func EditUser(ctx *context.APIContext) {
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
}
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
if err := user_service.UpdateAuth(ctx, ctx.Doer, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
@ -214,7 +217,7 @@ func EditUser(ctx *context.APIContext) {
}
if form.Email != nil {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.Doer, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
@ -246,17 +249,15 @@ func EditUser(ctx *context.APIContext) {
IsRestricted: optional.FromPtr(form.Restricted),
}
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, ctx.ContextUser, opts); err != nil {
if models.IsErrDeleteLastAdminUser(err) {
ctx.Error(http.StatusBadRequest, "LastAdmin", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
ctx.Error(http.StatusInternalServerError, "UpdateOrSetPrimaryEmail", err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
}
@ -298,7 +299,7 @@ func DeleteUser(ctx *context.APIContext) {
return
}
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if err := user_service.DeleteUser(ctx, ctx.Doer, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if models.IsErrUserOwnRepos(err) ||
models.IsErrUserHasOrgs(err) ||
models.IsErrUserOwnPackages(err) ||
@ -479,7 +480,7 @@ func RenameUser(ctx *context.APIContext) {
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
// Check if user name has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
if err := user_service.RenameUser(ctx, ctx.Doer, ctx.ContextUser, newName); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))

View File

@ -94,6 +94,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -122,6 +123,9 @@ func sudo() func(ctx *context.APIContext) {
}
return
}
audit.RecordUserImpersonation(ctx, ctx.Doer, user)
log.Trace("Sudo from (%s) to: %s", ctx.Doer.Name, user.Name)
ctx.Doer = user
} else {

View File

@ -106,7 +106,7 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer, ctx.Org.Organization.AsUser(), nil, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
@ -153,7 +153,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"))
err := secret_service.DeleteSecretByName(ctx, ctx.Doer, ctx.Org.Organization.AsUser(), nil, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)

View File

@ -13,7 +13,7 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)
// ListHooks list an organziation's webhooks
// ListHooks list an organization's webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/hooks organization orgListHooks
// ---

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org"
@ -277,6 +278,8 @@ func Create(ctx *context.APIContext) {
return
}
audit.RecordUserCreate(ctx, ctx.Doer, org.AsUser())
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
}
@ -342,8 +345,10 @@ func Edit(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgOption)
org := ctx.Org.Organization
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Doer, org.AsUser(), form.Email); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
return
}
@ -357,12 +362,12 @@ func Edit(ctx *context.APIContext) {
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, org.AsUser(), opts); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
return
}
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
}
// Delete an organization
@ -384,7 +389,7 @@ func Delete(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
if err := org.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
if err := org.DeleteOrganization(ctx, ctx.Doer, ctx.Org.Organization, false); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteOrganization", err)
return
}

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
@ -249,6 +250,8 @@ func CreateTeam(ctx *context.APIContext) {
return
}
audit.RecordOrganizationTeamAdd(ctx, ctx.Doer, ctx.Org.Organization, team)
apiTeam, err := convert.ToTeam(ctx, team, true)
if err != nil {
ctx.InternalServerError(err)
@ -284,6 +287,13 @@ func EditTeam(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditTeamOption)
team := ctx.Org.Team
org, err := organization.GetOrgByID(ctx, team.OrgID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetOrgByID", err)
return
}
if err := team.LoadUnits(ctx); err != nil {
ctx.InternalServerError(err)
return
@ -336,6 +346,11 @@ func EditTeam(ctx *context.APIContext) {
return
}
audit.RecordOrganizationTeamUpdate(ctx, ctx.Doer, org, team)
if isAuthChanged {
audit.RecordOrganizationTeamPermission(ctx, ctx.Doer, org, team)
}
apiTeam, err := convert.ToTeam(ctx, team)
if err != nil {
ctx.InternalServerError(err)
@ -362,10 +377,19 @@ func DeleteTeam(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
org, err := organization.GetOrgByID(ctx, ctx.Org.Team.OrgID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetOrgByID", err)
return
}
if err := models.DeleteTeam(ctx, ctx.Org.Team); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteTeam", err)
return
}
audit.RecordOrganizationTeamRemove(ctx, ctx.Doer, org, ctx.Org.Team)
ctx.Status(http.StatusNoContent)
}
@ -504,6 +528,15 @@ func AddTeamMember(ctx *context.APIContext) {
}
return
}
org, err := organization.GetOrgByID(ctx, ctx.Org.Team.OrgID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetOrgByID", err)
return
}
audit.RecordOrganizationTeamMemberAdd(ctx, ctx.Doer, org, ctx.Org.Team, u)
ctx.Status(http.StatusNoContent)
}
@ -541,6 +574,15 @@ func RemoveTeamMember(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
return
}
org, err := organization.GetOrgByID(ctx, ctx.Org.Team.OrgID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetOrgByID", err)
return
}
audit.RecordOrganizationTeamMemberRemove(ctx, ctx.Doer, org, ctx.Org.Team, u)
ctx.Status(http.StatusNoContent)
}
@ -700,7 +742,7 @@ func AddTeamRepository(ctx *context.APIContext) {
ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
return
}
if err := org_service.TeamAddRepository(ctx, ctx.Org.Team, repo); err != nil {
if err := org_service.TeamAddRepository(ctx, ctx.Doer, ctx.Org.Team, repo); err != nil {
ctx.Error(http.StatusInternalServerError, "TeamAddRepository", err)
return
}
@ -752,10 +794,11 @@ func RemoveTeamRepository(ctx *context.APIContext) {
ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
return
}
if err := repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, repo.ID); err != nil {
if err := repo_service.RemoveRepositoryFromTeam(ctx, ctx.Doer, ctx.Org.Team, repo); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveRepository", err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -117,12 +117,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
owner := ctx.Repo.Owner
repo := ctx.Repo.Repository
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, owner.ID, repo.ID, ctx.Params("secretname"), opt.Data)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer, ctx.Repo.Owner, ctx.Repo.Repository, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
@ -174,10 +171,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
owner := ctx.Repo.Owner
repo := ctx.Repo.Repository
err := secret_service.DeleteSecretByName(ctx, owner.ID, repo.ID, ctx.Params("secretname"))
err := secret_service.DeleteSecretByName(ctx, ctx.Doer, ctx.Repo.Owner, ctx.Repo.Repository, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)

View File

@ -16,6 +16,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
@ -186,11 +187,16 @@ func AddCollaborator(ctx *context.APIContext) {
return
}
audit.RecordRepositoryCollaboratorAdd(ctx, ctx.Doer, ctx.Repo.Repository, collaborator)
if form.Permission != nil {
if err := repo_model.ChangeCollaborationAccessMode(ctx, ctx.Repo.Repository, collaborator.ID, perm.ParseAccessMode(*form.Permission)); err != nil {
accessMode := perm.ParseAccessMode(*form.Permission)
if err := repo_model.ChangeCollaborationAccessMode(ctx, ctx.Repo.Repository, collaborator.ID, accessMode); err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeCollaborationAccessMode", err)
return
}
audit.RecordRepositoryCollaboratorAccess(ctx, ctx.Doer, ctx.Repo.Repository, collaborator, accessMode)
}
ctx.Status(http.StatusNoContent)
@ -241,6 +247,9 @@ func DeleteCollaborator(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
return
}
audit.RecordRepositoryCollaboratorRemove(ctx, ctx.Doer, ctx.Repo.Repository, collaborator)
ctx.Status(http.StatusNoContent)
}

View File

@ -7,15 +7,11 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
webhook_service "code.gitea.io/gitea/services/webhook"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
SetUp: func() error {
setting.LoadQueueSettings()
return webhook_service.Init()
},
SetUp: webhook_service.Init,
})
}

View File

@ -33,6 +33,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/issue"
@ -748,6 +749,10 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
return err
}
if visibilityChanged {
audit.RecordRepositoryVisibility(ctx, ctx.Doer, repo)
}
log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name)
return nil
}

View File

@ -205,13 +205,13 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
ctx.Error(http.StatusUnprocessableEntity, "alreadyAdded", fmt.Errorf("team '%s' is already added to repo", team.Name))
return
}
err = org_service.TeamAddRepository(ctx, team, ctx.Repo.Repository)
err = org_service.TeamAddRepository(ctx, ctx.Doer, team, ctx.Repo.Repository)
} else {
if !repoHasTeam {
ctx.Error(http.StatusUnprocessableEntity, "notAdded", fmt.Errorf("team '%s' was not added to repo", team.Name))
return
}
err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID)
err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Doer, team, ctx.Repo.Repository)
}
if err != nil {
ctx.InternalServerError(err)

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
@ -235,5 +236,11 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
}
return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository)
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
return err
}
audit.RecordRepositoryTransferCancel(ctx, ctx.Doer, ctx.Repo.Repository)
return nil
}

View File

@ -49,7 +49,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.Params("secretname"), opt.Data)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer, ctx.Doer, nil, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
@ -91,7 +91,7 @@ func DeleteSecret(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.Params("secretname"))
err := secret_service.DeleteSecretByName(ctx, ctx.Doer, ctx.Doer, nil, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)

View File

@ -14,8 +14,10 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@ -124,6 +126,9 @@ func CreateAccessToken(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "NewAccessToken", err)
return
}
audit.RecordUserAccessTokenAdd(ctx, ctx.Doer, ctx.Doer, t)
ctx.JSON(http.StatusCreated, &api.AccessToken{
Name: t.Name,
Token: t.Token,
@ -163,6 +168,7 @@ func DeleteAccessToken(ctx *context.APIContext) {
token := ctx.Params(":id")
tokenID, _ := strconv.ParseInt(token, 0, 64)
var t *auth_model.AccessToken
if tokenID == 0 {
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{
Name: token,
@ -178,18 +184,25 @@ func DeleteAccessToken(ctx *context.APIContext) {
ctx.NotFound()
return
case 1:
tokenID = tokens[0].ID
t = tokens[0]
default:
ctx.Error(http.StatusUnprocessableEntity, "DeleteAccessTokenByID", fmt.Errorf("multiple matches for token name '%s'", token))
return
}
} else {
var err error
t, _, err = db.GetByID[auth_model.AccessToken](ctx, tokenID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetByID", err)
return
}
}
if tokenID == 0 {
ctx.Error(http.StatusInternalServerError, "Invalid TokenID", nil)
if t == nil {
ctx.NotFound()
return
}
if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil {
if err := auth_model.DeleteAccessTokenByID(ctx, t.ID, ctx.ContextUser.ID); err != nil {
if auth_model.IsErrAccessTokenNotExist(err) {
ctx.NotFound()
} else {
@ -198,6 +211,8 @@ func DeleteAccessToken(ctx *context.APIContext) {
return
}
audit.RecordUserAccessTokenRemove(ctx, ctx.Doer, ctx.Doer, t)
ctx.Status(http.StatusNoContent)
}
@ -239,6 +254,8 @@ func CreateOauth2Application(ctx *context.APIContext) {
}
app.ClientSecret = secret
audit.RecordOAuth2ApplicationAdd(ctx, ctx.Doer, ctx.Doer, app)
ctx.JSON(http.StatusCreated, convert.ToOAuth2Application(app))
}
@ -300,8 +317,18 @@ func DeleteOauth2Application(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
appID := ctx.ParamsInt64(":id")
if err := auth_model.DeleteOAuth2Application(ctx, appID, ctx.Doer.ID); err != nil {
app, err := auth_model.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetOAuth2ApplicationByID", err)
}
return
}
if err := auth_model.DeleteOAuth2Application(ctx, app.ID, ctx.Doer.ID); err != nil {
if auth_model.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound()
} else {
@ -310,6 +337,8 @@ func DeleteOauth2Application(ctx *context.APIContext) {
return
}
audit.RecordOAuth2ApplicationRemove(ctx, ctx.Doer, ctx.Doer, app)
ctx.Status(http.StatusNoContent)
}
@ -401,5 +430,7 @@ func UpdateOauth2Application(ctx *context.APIContext) {
return
}
audit.RecordOAuth2ApplicationUpdate(ctx, ctx.Doer, ctx.Doer, app)
ctx.JSON(http.StatusOK, convert.ToOAuth2Application(app))
}

View File

@ -63,7 +63,7 @@ func AddEmail(ctx *context.APIContext) {
return
}
if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
if err := user_service.AddEmailAddresses(ctx, ctx.Doer, ctx.Doer, form.Emails); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
@ -120,7 +120,7 @@ func DeleteEmail(ctx *context.APIContext) {
return
}
if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil {
if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, ctx.Doer, form.Emails); err != nil {
if user_model.IsErrEmailAddressNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err)
} else {

View File

@ -56,7 +56,7 @@ func UpdateUserSettings(ctx *context.APIContext) {
KeepEmailPrivate: optional.FromPtr(form.HideEmail),
KeepActivityPrivate: optional.FromPtr(form.HideActivity),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
ctx.InternalServerError(err)
return
}

View File

@ -36,6 +36,7 @@ import (
web_routers "code.gitea.io/gitea/routers/web"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/automerge"
@ -172,6 +173,8 @@ func InitWebInstalled(ctx context.Context) {
actions_service.Init()
mustInit(audit.Init)
// Finally start up the cron
cron.NewContext(ctx)
}

View File

@ -34,6 +34,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/audit"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -566,6 +567,8 @@ func SubmitInstall(ctx *context.Context) {
u, _ = user_model.GetUserByName(ctx, u.Name)
}
audit.RecordUserCreate(ctx, u, u)
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
if err != nil {
ctx.ServerError("CreateAuthTokenForUserID", err)

View File

@ -25,6 +25,7 @@ import (
timeutil "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
gitea_context "code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
@ -228,6 +229,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
})
return
}
if isPrivate.Has() {
audit.RecordRepositoryVisibility(ctx, pusher, repo)
}
}
}

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
@ -20,9 +21,10 @@ var (
tplSettingsOauth2ApplicationEdit base.TplName = "admin/applications/oauth2_edit"
)
func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
func newOAuth2CommonHandlers(doer *user_model.User) *user_setting.OAuth2CommonHandlers {
return &user_setting.OAuth2CommonHandlers{
OwnerID: 0,
Doer: doer,
Owner: nil,
BasePathList: fmt.Sprintf("%s/admin/applications", setting.AppSubURL),
BasePathEditPrefix: fmt.Sprintf("%s/admin/applications/oauth2", setting.AppSubURL),
TplAppEdit: tplSettingsOauth2ApplicationEdit,
@ -51,7 +53,7 @@ func ApplicationsPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsAdminApplications"] = true
oa := newOAuth2CommonHandlers()
oa := newOAuth2CommonHandlers(ctx.Doer)
oa.AddApp(ctx)
}
@ -59,7 +61,7 @@ func ApplicationsPost(ctx *context.Context) {
func EditApplication(ctx *context.Context) {
ctx.Data["PageIsAdminApplications"] = true
oa := newOAuth2CommonHandlers()
oa := newOAuth2CommonHandlers(ctx.Doer)
oa.EditShow(ctx)
}
@ -68,7 +70,7 @@ func EditApplicationPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.applications")
ctx.Data["PageIsAdminApplications"] = true
oa := newOAuth2CommonHandlers()
oa := newOAuth2CommonHandlers(ctx.Doer)
oa.EditSave(ctx)
}
@ -77,13 +79,13 @@ func ApplicationsRegenerateSecret(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsAdminApplications"] = true
oa := newOAuth2CommonHandlers()
oa := newOAuth2CommonHandlers(ctx.Doer)
oa.RegenerateSecret(ctx)
}
// DeleteApplication deletes the given oauth2 application
func DeleteApplication(ctx *context.Context) {
oa := newOAuth2CommonHandlers()
oa := newOAuth2CommonHandlers(ctx.Doer)
oa.DeleteApp(ctx)
}

View File

@ -0,0 +1,53 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
audit_model "code.gitea.io/gitea/models/audit"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
const (
tplAuditLogs base.TplName = "admin/audit/list"
)
func ViewAuditLogs(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor.audit.title")
ctx.Data["PageIsAdminMonitorAudit"] = true
page := ctx.FormInt("page")
if page < 1 {
page = 1
}
opts := &audit_model.EventSearchOptions{
Sort: ctx.FormString("sort"),
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.Admin.NoticePagingNum,
},
}
ctx.Data["AuditSort"] = opts.Sort
evs, total, err := audit.FindEvents(ctx, opts)
if err != nil {
ctx.ServerError("", err)
return
}
ctx.Data["AuditEvents"] = evs
pager := context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
pager.AddParamString("sort", opts.Sort)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplAuditLogs)
}

View File

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/ldap"
"code.gitea.io/gitea/services/auth/source/oauth2"
@ -301,13 +302,15 @@ func NewAuthSourcePost(ctx *context.Context) {
return
}
if err := auth.CreateSource(ctx, &auth.Source{
source := &auth.Source{
Type: auth.Type(form.Type),
Name: form.Name,
IsActive: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled,
Cfg: config,
}); err != nil {
}
if err := auth.CreateSource(ctx, source); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
@ -321,6 +324,8 @@ func NewAuthSourcePost(ctx *context.Context) {
return
}
audit.RecordSystemAuthenticationSourceAdd(ctx, ctx.Doer, source)
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
@ -434,6 +439,9 @@ func EditAuthSourcePost(ctx *context.Context) {
}
return
}
audit.RecordSystemAuthenticationSourceUpdate(ctx, ctx.Doer, source)
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
@ -448,7 +456,7 @@ func DeleteAuthSource(ctx *context.Context) {
return
}
if err = auth_service.DeleteSource(ctx, source); err != nil {
if err = auth_service.DeleteSource(ctx, ctx.Doer, source); err != nil {
if auth.IsErrSourceInUse(err) {
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
} else {
@ -457,6 +465,7 @@ func DeleteAuthSource(ctx *context.Context) {
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.Params(":authid")))
return
}
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
@ -111,26 +112,34 @@ func ActivateEmail(ctx *context.Context) {
uid := ctx.FormInt64("uid")
email := ctx.FormString("email")
primary, okp := truefalse[ctx.FormString("primary")]
activate, oka := truefalse[ctx.FormString("activate")]
if uid == 0 || len(email) == 0 || !okp || !oka {
if uid == 0 || len(email) == 0 || !oka {
ctx.Error(http.StatusBadRequest)
return
}
log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
log.Info("Changing activation for User ID: %d, email: %s to %v", uid, email, activate)
if err := user_model.ActivateUserEmail(ctx, uid, email, activate); err != nil {
log.Error("ActivateUserEmail(%v,%v,%v): %v", uid, email, activate, err)
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
} else {
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
}
u, err := user_model.GetUserByID(ctx, uid)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
} else {
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"))
if email, err := user_model.ActivateUserEmail(ctx, uid, email, activate); err != nil {
log.Error("ActivateUserEmail(%v,%v,%v): %v", uid, email, activate, err)
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
} else {
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
}
} else {
if activate {
audit.RecordUserEmailActivate(ctx, ctx.Doer, u, email)
}
log.Info("Activation for User ID: %d, email: %s changed to %v", uid, email, activate)
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
}
}
redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
@ -61,9 +62,17 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, ctx.FormInt64("id"))
if err != nil {
ctx.ServerError("GetSystemOrDefaultWebhook", err)
return
}
if err := webhook.DeleteDefaultSystemWebhook(ctx, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
} else {
audit.RecordWebhookRemove(ctx, ctx.Doer, nil, nil, hook)
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}

View File

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
@ -207,6 +208,8 @@ func NewUserPost(ctx *context.Context) {
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
}
audit.RecordUserCreate(ctx, ctx.Doer, u)
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
@ -348,7 +351,7 @@ func EditUserPost(ctx *context.Context) {
}
if form.UserName != "" {
if err := user_service.RenameUser(ctx, u, form.UserName); err != nil {
if err := user_service.RenameUser(ctx, ctx.Doer, u, form.UserName); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
ctx.Data["Err_UserName"] = true
@ -391,7 +394,7 @@ func EditUserPost(ctx *context.Context) {
authOpts.LoginSource = optional.Some(authSource)
}
if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
if err := user_service.UpdateAuth(ctx, ctx.Doer, u, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
@ -406,13 +409,13 @@ func EditUserPost(ctx *context.Context) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
default:
ctx.ServerError("UpdateUser", err)
ctx.ServerError("UpdateAuth", err)
}
return
}
if form.Email != "" {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.Doer, u, form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
@ -445,7 +448,7 @@ func EditUserPost(ctx *context.Context) {
Language: optional.Some(form.Language),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, u, opts); err != nil {
if models.IsErrDeleteLastAdminUser(err) {
ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
} else {
@ -499,7 +502,7 @@ func DeleteUser(ctx *context.Context) {
return
}
if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil {
if err = user_service.DeleteUser(ctx, ctx.Doer, u, ctx.FormBool("purge")); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
@ -52,6 +53,13 @@ func TwoFactorPost(ctx *context.Context) {
}
id := idSess.(int64)
u, err := user_model.GetUserByID(ctx, id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
twofa, err := auth.GetTwoFactorByUID(ctx, id)
if err != nil {
ctx.ServerError("UserSignIn", err)
@ -67,11 +75,6 @@ func TwoFactorPost(ctx *context.Context) {
if ok && twofa.LastUsedPasscode != form.Passcode {
remember := ctx.Session.Get("twofaRemember").(bool)
u, err := user_model.GetUserByID(ctx, id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
if ctx.Session.Get("linkAccount") != nil {
err = externalaccount.LinkAccountFromStore(ctx, ctx.Session, u)
@ -91,6 +94,8 @@ func TwoFactorPost(ctx *context.Context) {
return
}
audit.RecordUserAuthenticationFailTwoFactor(ctx, u)
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
}

View File

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/audit"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
@ -109,7 +110,7 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
opts := &user_service.UpdateOptions{
Language: optional.Some(ctx.Locale.Language()),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if err := user_service.UpdateUser(ctx, u, u, opts); err != nil {
return err
}
}
@ -347,7 +348,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
opts := &user_service.UpdateOptions{
Language: optional.Some(ctx.Locale.Language()),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if err := user_service.UpdateUser(ctx, u, u, opts); err != nil {
ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
return setting.AppSubURL + "/"
}
@ -363,7 +364,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Csrf.DeleteCookie(ctx)
// Register last login
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
if err := user_service.UpdateUser(ctx, u, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
ctx.ServerError("UpdateUser", err)
return setting.AppSubURL + "/"
}
@ -599,6 +600,9 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us
}
return false
}
audit.RecordUserCreate(ctx, user_model.NewAuthenticationSourceUser(), u)
log.Trace("Account created: %s", u.Name)
return true
}
@ -614,7 +618,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
IsAdmin: optional.Some(true),
SetLastLogin: true,
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if err := user_service.UpdateUser(ctx, u, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return false
}
@ -785,12 +789,16 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
if err := user_model.ActivateUserEmail(ctx, user.ID, user.Email, true); err != nil {
email, err := user_model.ActivateUserEmail(ctx, user.ID, user.Email, true)
if err != nil {
log.Error("Unable to activate email for user: %-v with email: %s: %v", user, user.Email, err)
ctx.ServerError("ActivateUserEmail", err)
return
}
audit.RecordUserActive(ctx, user, user)
audit.RecordUserEmailActivate(ctx, user, user, email)
log.Trace("User activated: %s", user.Name)
if err := updateSession(ctx, nil, map[string]any{
@ -807,7 +815,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
if err := user_service.UpdateUser(ctx, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
if err := user_service.UpdateUser(ctx, user, user, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
@ -828,7 +836,7 @@ func ActivateEmail(ctx *context.Context) {
emailStr := ctx.FormString("email")
// Verify code.
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
if user, email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); user != nil && email != nil {
if err := user_model.ActivateEmail(ctx, email); err != nil {
ctx.ServerError("ActivateEmail", err)
}
@ -836,12 +844,10 @@ func ActivateEmail(ctx *context.Context) {
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
} else {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + user.LowerName)
audit.RecordUserEmailActivate(ctx, user, user, email)
}
// FIXME: e-mail verification does not require the user to be logged in,

View File

@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/audit"
auth_service "code.gitea.io/gitea/services/auth"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2"
@ -546,6 +547,17 @@ func GrantApplicationOAuth(ctx *context.Context) {
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
owner, err := user_model.GetUserByID(ctx, app.UID)
if err != nil && !errors.Is(err, util.ErrNotExist) {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "cannot find user",
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}
grant, err := app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
if err != nil {
handleAuthorizeError(ctx, AuthorizeError{
@ -555,6 +567,9 @@ func GrantApplicationOAuth(ctx *context.Context) {
}, form.RedirectURI)
return
}
audit.RecordUserOAuth2ApplicationGrant(ctx, ctx.Doer, owner, app, grant)
if len(form.Nonce) > 0 {
err := grant.SetNonce(ctx, form.Nonce)
if err != nil {
@ -1141,7 +1156,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
SetLastLogin: true,
}
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if err := user_service.UpdateUser(ctx, user_model.NewAuthenticationSourceUser(), u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
@ -1178,7 +1193,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
opts := &user_service.UpdateOptions{}
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
if err := user_service.UpdateUser(ctx, user_model.NewAuthenticationSourceUser(), u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -280,6 +281,8 @@ func ConnectOpenIDPost(ctx *context.Context) {
return
}
audit.RecordUserOpenIDAdd(ctx, u, u, userOID)
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
@ -384,6 +387,8 @@ func RegisterOpenIDPost(ctx *context.Context) {
return
}
audit.RecordUserOpenIDAdd(ctx, u, u, userOID)
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
log.Trace("Session stored openid-remember: %t", remember)
handleSignIn(ctx, u, remember)

View File

@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
@ -88,6 +89,8 @@ func ForgotPasswdPost(ctx *context.Context) {
mailer.SendResetPasswordMail(u)
audit.RecordUserPasswordResetRequest(ctx, user_model.NewGhostUser(), u)
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
@ -185,6 +188,8 @@ func ResetPasswdPost(ctx *context.Context) {
return
}
if !ok || twofa.LastUsedPasscode == passcode {
audit.RecordUserAuthenticationFailTwoFactor(ctx, u)
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Passcode"] = true
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
@ -203,7 +208,7 @@ func ResetPasswdPost(ctx *context.Context) {
Password: optional.Some(ctx.FormString("password")),
MustChangePassword: optional.Some(false),
}
if err := user_service.UpdateAuth(ctx, u, opts); err != nil {
if err := user_service.UpdateAuth(ctx, ctx.Doer, u, opts); err != nil {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
switch {
@ -221,7 +226,6 @@ func ResetPasswdPost(ctx *context.Context) {
return
}
log.Trace("User password reset: %s", u.Name)
ctx.Data["IsResetFailed"] = true
remember := len(ctx.FormString("remember")) != 0
@ -285,7 +289,7 @@ func MustChangePasswordPost(ctx *context.Context) {
Password: optional.Some(form.Password),
MustChangePassword: optional.Some(false),
}
if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
if err := user_service.UpdateAuth(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
@ -307,8 +311,6 @@ func MustChangePasswordPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
log.Trace("User updated password: %s", ctx.Doer.Name)
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" {
middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToCurrentSite(redirectTo)

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@ -74,6 +75,9 @@ func CreatePost(ctx *context.Context) {
}
return
}
audit.RecordUserCreate(ctx, ctx.Doer, org.AsUser())
log.Trace("Organization created: %s", org.Name)
ctx.Redirect(org.AsUser().DashboardLink())

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
org_service "code.gitea.io/gitea/services/org"
@ -73,7 +74,7 @@ func SettingsPost(ctx *context.Context) {
org := ctx.Org.Organization
if org.Name != form.Name {
if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
if err := user_service.RenameUser(ctx, ctx.Doer, org.AsUser(), form.Name); err != nil {
if user_model.IsErrUserAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
@ -93,13 +94,15 @@ func SettingsPost(ctx *context.Context) {
}
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Doer, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
return
}
}
oldVisibility := org.Visibility
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
Description: optional.Some(form.Description),
@ -111,16 +114,13 @@ func SettingsPost(ctx *context.Context) {
if ctx.Doer.IsAdmin {
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
}
visibilityChanged := org.Visibility != form.Visibility
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, org.AsUser(), opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
// update forks visibility
if visibilityChanged {
if org.Visibility != oldVisibility {
repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos},
})
@ -177,7 +177,7 @@ func SettingsDelete(ctx *context.Context) {
return
}
if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
if err := org_service.DeleteOrganization(ctx, ctx.Doer, ctx.Org.Organization, false); err != nil {
if models.IsErrUserOwnRepos(err) {
ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
@ -230,9 +230,17 @@ func Webhooks(ctx *context.Context) {
// DeleteWebhook response for delete webhook
func DeleteWebhook(ctx *context.Context) {
hook, err := webhook.GetWebhookByOwnerID(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id"))
if err != nil {
ctx.ServerError("GetWebhookByOwnerID", err)
return
}
if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error())
} else {
audit.RecordWebhookRemove(ctx, ctx.Doer, ctx.Org.Organization.AsUser(), nil, hook)
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}

View File

@ -0,0 +1,56 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
audit_model "code.gitea.io/gitea/models/audit"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
const (
tplAuditLogs base.TplName = "org/settings/audit_logs"
)
func ViewAuditLogs(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor.audit.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsAudit"] = true
page := ctx.FormInt("page")
if page < 1 {
page = 1
}
opts := &audit_model.EventSearchOptions{
Sort: ctx.FormString("sort"),
ScopeType: audit_model.TypeOrganization,
ScopeID: ctx.ContextUser.ID,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.Admin.NoticePagingNum,
},
}
ctx.Data["AuditSort"] = opts.Sort
evs, total, err := audit.FindEvents(ctx, opts)
if err != nil {
ctx.ServerError("", err)
return
}
ctx.Data["AuditEvents"] = evs
pager := context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
pager.AddParamString("sort", opts.Sort)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplAuditLogs)
}

View File

@ -9,6 +9,8 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
organization_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
@ -21,11 +23,12 @@ const (
tplSettingsOAuthApplicationEdit base.TplName = "org/settings/applications_oauth2_edit"
)
func newOAuth2CommonHandlers(org *context.Organization) *user_setting.OAuth2CommonHandlers {
func newOAuth2CommonHandlers(doer *user_model.User, org *organization_model.Organization) *user_setting.OAuth2CommonHandlers {
return &user_setting.OAuth2CommonHandlers{
OwnerID: org.Organization.ID,
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),
Doer: doer,
Owner: organization_model.UserFromOrg(org),
BasePathList: fmt.Sprintf("%s/org/%s/settings/applications", setting.AppSubURL, org.Name),
BasePathEditPrefix: fmt.Sprintf("%s/org/%s/settings/applications/oauth2", setting.AppSubURL, org.Name),
TplAppEdit: tplSettingsOAuthApplicationEdit,
}
}
@ -60,7 +63,7 @@ func OAuthApplicationsPost(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Org)
oa := newOAuth2CommonHandlers(ctx.Doer, ctx.Org.Organization)
oa.AddApp(ctx)
}
@ -69,7 +72,7 @@ func OAuth2ApplicationShow(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Org)
oa := newOAuth2CommonHandlers(ctx.Doer, ctx.Org.Organization)
oa.EditShow(ctx)
}
@ -79,7 +82,7 @@ func OAuth2ApplicationEdit(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Org)
oa := newOAuth2CommonHandlers(ctx.Doer, ctx.Org.Organization)
oa.EditSave(ctx)
}
@ -89,13 +92,13 @@ func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsApplications"] = true
oa := newOAuth2CommonHandlers(ctx.Org)
oa := newOAuth2CommonHandlers(ctx.Doer, ctx.Org.Organization)
oa.RegenerateSecret(ctx)
}
// DeleteOAuth2Application deletes the given oauth2 application
func DeleteOAuth2Application(ctx *context.Context) {
oa := newOAuth2CommonHandlers(ctx.Org)
oa := newOAuth2CommonHandlers(ctx.Doer, ctx.Org.Organization)
oa.DeleteApp(ctx)
}

View File

@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
@ -79,6 +80,9 @@ func TeamsAction(ctx *context.Context) {
return
}
err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
if err == nil {
audit.RecordOrganizationTeamMemberAdd(ctx, ctx.Doer, ctx.Org.Organization, ctx.Org.Team, ctx.Doer)
}
case "leave":
err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
if err != nil {
@ -92,6 +96,8 @@ func TeamsAction(ctx *context.Context) {
})
return
}
} else {
audit.RecordOrganizationTeamMemberRemove(ctx, ctx.Doer, ctx.Org.Organization, ctx.Org.Team, ctx.Doer)
}
checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/")
return
@ -119,6 +125,8 @@ func TeamsAction(ctx *context.Context) {
})
return
}
} else {
audit.RecordOrganizationTeamMemberRemove(ctx, ctx.Doer, ctx.Org.Organization, ctx.Org.Team, user)
}
checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/"+url.PathEscape(ctx.Org.Team.LowerName))
return
@ -163,6 +171,9 @@ func TeamsAction(ctx *context.Context) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
err = models.AddTeamMember(ctx, ctx.Org.Team, u)
if err == nil {
audit.RecordOrganizationTeamMemberAdd(ctx, ctx.Doer, ctx.Org.Organization, ctx.Org.Team, u)
}
}
page = "team"
@ -233,13 +244,10 @@ func TeamsRepoAction(ctx *context.Context) {
return
}
var err error
action := ctx.Params(":action")
switch action {
case "add":
repoName := path.Base(ctx.FormString("repo_name"))
var repo *repo_model.Repository
repo, err = repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName)
repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, path.Base(ctx.FormString("repo_name")))
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo"))
@ -249,19 +257,44 @@ func TeamsRepoAction(ctx *context.Context) {
ctx.ServerError("GetRepositoryByName", err)
return
}
err = org_service.TeamAddRepository(ctx, ctx.Org.Team, repo)
if err := org_service.TeamAddRepository(ctx, ctx.Doer, ctx.Org.Team, repo); err != nil {
ctx.ServerError("TeamAddRepository "+ctx.Org.Team.Name, err)
return
}
case "remove":
err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid"))
repo, err := repo_model.GetRepositoryByID(ctx, ctx.FormInt64("repoid"))
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
}
if err := repo_service.RemoveRepositoryFromTeam(ctx, ctx.Doer, ctx.Org.Team, repo); err != nil {
ctx.ServerError("RemoveRepositoryFromTeam "+ctx.Org.Team.Name, err)
return
}
case "addall":
err = models.AddAllRepositories(ctx, ctx.Org.Team)
case "removeall":
err = models.RemoveAllRepositories(ctx, ctx.Org.Team)
}
added, err := models.AddAllRepositories(ctx, ctx.Org.Team)
if err != nil {
ctx.ServerError("AddAllRepositories "+ctx.Org.Team.Name, err)
return
}
if err != nil {
log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err)
ctx.ServerError("TeamsRepoAction", err)
return
for _, repo := range added {
audit.RecordRepositoryCollaboratorTeamAdd(ctx, ctx.Doer, repo, ctx.Org.Team)
}
case "removeall":
if err := ctx.Org.Team.LoadRepositories(ctx); err != nil {
ctx.ServerError("LoadRepositories "+ctx.Org.Team.Name, err)
return
}
if err := models.RemoveAllRepositories(ctx, ctx.Org.Team); err != nil {
ctx.ServerError("RemoveAllRepositories "+ctx.Org.Team.Name, err)
return
}
for _, repo := range ctx.Org.Team.Repos {
audit.RecordRepositoryCollaboratorTeamRemove(ctx, ctx.Doer, repo, ctx.Org.Team)
}
}
if action == "addall" || action == "removeall" {
@ -368,6 +401,9 @@ func NewTeamPost(ctx *context.Context) {
}
return
}
audit.RecordOrganizationTeamAdd(ctx, ctx.Doer, ctx.Org.Organization, t)
log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name)
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName))
}
@ -546,6 +582,12 @@ func EditTeamPost(ctx *context.Context) {
}
return
}
audit.RecordOrganizationTeamUpdate(ctx, ctx.Doer, ctx.Org.Organization, t)
if isAuthChanged {
audit.RecordOrganizationTeamPermission(ctx, ctx.Doer, ctx.Org.Organization, t)
}
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName))
}
@ -554,6 +596,8 @@ func DeleteTeam(ctx *context.Context) {
if err := models.DeleteTeam(ctx, ctx.Org.Team); err != nil {
ctx.Flash.Error("DeleteTeam: " + err.Error())
} else {
audit.RecordOrganizationTeamRemove(ctx, ctx.Doer, ctx.Org.Organization, ctx.Org.Team)
ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success"))
}
@ -598,6 +642,8 @@ func TeamInvitePost(ctx *context.Context) {
return
}
audit.RecordOrganizationTeamMemberAdd(ctx, ctx.Doer, org, team, ctx.Doer)
if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil {
log.Error("RemoveInviteByID: %v", err)
}

View File

@ -61,7 +61,7 @@ func SetDiffViewStyle(ctx *context.Context) {
opts := &user_service.UpdateOptions{
DiffViewStyle: optional.Some(style),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
}
}

View File

@ -30,6 +30,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
@ -402,6 +403,9 @@ func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
return err
}
audit.RecordRepositoryTransferCancel(ctx, ctx.Doer, ctx.Repo.Repository)
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
}

View File

@ -0,0 +1,55 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
audit_model "code.gitea.io/gitea/models/audit"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
const (
tplAuditLogs base.TplName = "repo/settings/audit_logs"
)
func ViewAuditLogs(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor.audit.title")
ctx.Data["PageIsSettingsAudit"] = true
page := ctx.FormInt("page")
if page < 1 {
page = 1
}
opts := &audit_model.EventSearchOptions{
ScopeType: audit_model.TypeRepository,
ScopeID: ctx.Repo.Repository.ID,
Sort: ctx.FormString("sort"),
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.Admin.NoticePagingNum,
},
}
ctx.Data["AuditSort"] = opts.Sort
evs, total, err := audit.FindEvents(ctx, opts)
if err != nil {
ctx.ServerError("", err)
return
}
ctx.Data["AuditEvents"] = evs
pager := context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
pager.AddParamString("sort", opts.Sort)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplAuditLogs)
}

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/mailer"
org_service "code.gitea.io/gitea/services/org"
@ -114,19 +115,32 @@ func CollaborationPost(ctx *context.Context) {
mailer.SendCollaboratorMail(u, ctx.Doer, ctx.Repo.Repository)
}
audit.RecordRepositoryCollaboratorAdd(ctx, ctx.Doer, ctx.Repo.Repository, u)
ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
}
// ChangeCollaborationAccessMode response for changing access of a collaboration
func ChangeCollaborationAccessMode(ctx *context.Context) {
u, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
if err != nil {
log.Error("GetUserByID: %v", err)
return
}
accessMode := perm.AccessMode(ctx.FormInt("mode"))
if err := repo_model.ChangeCollaborationAccessMode(
ctx,
ctx.Repo.Repository,
ctx.FormInt64("uid"),
perm.AccessMode(ctx.FormInt("mode"))); err != nil {
u.ID,
accessMode); err != nil {
log.Error("ChangeCollaborationAccessMode: %v", err)
return
}
audit.RecordRepositoryCollaboratorAccess(ctx, ctx.Doer, ctx.Repo.Repository, u, accessMode)
}
// DeleteCollaboration delete a collaboration for a repository
@ -142,6 +156,8 @@ func DeleteCollaboration(ctx *context.Context) {
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Flash.Error("DeleteCollaboration: " + err.Error())
} else {
audit.RecordRepositoryCollaboratorRemove(ctx, ctx.Doer, ctx.Repo.Repository, collaborator)
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
}
}
@ -186,7 +202,7 @@ func AddTeamPost(ctx *context.Context) {
return
}
if err = org_service.TeamAddRepository(ctx, team, ctx.Repo.Repository); err != nil {
if err = org_service.TeamAddRepository(ctx, ctx.Doer, team, ctx.Repo.Repository); err != nil {
ctx.ServerError("TeamAddRepository", err)
return
}
@ -209,11 +225,13 @@ func DeleteTeam(ctx *context.Context) {
return
}
if err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID); err != nil {
if err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Doer, team, ctx.Repo.Repository); err != nil {
ctx.ServerError("team.RemoveRepositorys", err)
return
}
audit.RecordRepositoryCollaboratorTeamRemove(ctx, ctx.Doer, ctx.Repo.Repository, team)
ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
}

View File

@ -34,7 +34,7 @@ func SetDefaultBranchPost(ctx *context.Context) {
}
branch := ctx.FormString("branch")
if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, branch); err != nil {
if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branch); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.Status(http.StatusNotFound)

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@ -92,6 +93,8 @@ func DeployKeysPost(ctx *context.Context) {
return
}
audit.RecordRepositoryDeployKeyAdd(ctx, ctx.Doer, ctx.Repo.Repository, key)
log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
@ -234,6 +235,8 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
isNewProtectedBranch := protectBranch.ID == 0
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
@ -247,6 +250,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
return
}
if isNewProtectedBranch {
audit.RecordRepositoryBranchProtectionAdd(ctx, ctx.Doer, ctx.Repo.Repository, protectBranch)
} else {
audit.RecordRepositoryBranchProtectionUpdate(ctx, ctx.Doer, ctx.Repo.Repository, protectBranch)
}
// FIXME: since we only need to recheck files protected rules, we could improve this
matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName)
if err != nil {
@ -292,6 +301,8 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) {
return
}
audit.RecordRepositoryBranchProtectionRemove(ctx, ctx.Doer, ctx.Repo.Repository, rule)
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
}

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@ -63,6 +64,8 @@ func NewProtectedTagPost(ctx *context.Context) {
return
}
audit.RecordRepositoryTagProtectionAdd(ctx, ctx.Doer, repo, pt)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
}
@ -116,6 +119,8 @@ func EditProtectedTagPost(ctx *context.Context) {
return
}
audit.RecordRepositoryTagProtectionUpdate(ctx, ctx.Doer, ctx.Repo.Repository, pt)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}
@ -132,6 +137,8 @@ func DeleteProtectedTagPost(ctx *context.Context) {
return
}
audit.RecordRepositoryTagProtectionRemove(ctx, ctx.Doer, ctx.Repo.Repository, pt)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}

View File

@ -7,6 +7,8 @@ import (
"errors"
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
@ -22,8 +24,8 @@ const (
)
type secretsCtx struct {
OwnerID int64
RepoID int64
Owner *user_model.User
Repo *repo_model.Repository
IsRepo bool
IsOrg bool
IsUser bool
@ -34,8 +36,8 @@ type secretsCtx struct {
func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &secretsCtx{
OwnerID: 0,
RepoID: ctx.Repo.Repository.ID,
Owner: nil,
Repo: ctx.Repo.Repository,
IsRepo: true,
SecretsTemplate: tplRepoSecrets,
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/secrets",
@ -49,8 +51,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
return nil, nil
}
return &secretsCtx{
OwnerID: ctx.ContextUser.ID,
RepoID: 0,
Owner: ctx.ContextUser,
Repo: nil,
IsOrg: true,
SecretsTemplate: tplOrgSecrets,
RedirectLink: ctx.Org.OrgLink + "/settings/actions/secrets",
@ -59,8 +61,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
if ctx.Data["PageIsUserSettings"] == true {
return &secretsCtx{
OwnerID: ctx.Doer.ID,
RepoID: 0,
Owner: ctx.Doer,
Repo: nil,
IsUser: true,
SecretsTemplate: tplUserSecrets,
RedirectLink: setting.AppSubURL + "/user/settings/actions/secrets",
@ -85,7 +87,7 @@ func Secrets(ctx *context.Context) {
ctx.Data["DisableSSH"] = setting.SSH.Disabled
}
shared.SetSecretsContext(ctx, sCtx.OwnerID, sCtx.RepoID)
shared.SetSecretsContext(ctx, sCtx.Owner, sCtx.Repo)
if ctx.Written() {
return
}
@ -106,8 +108,9 @@ func SecretsPost(ctx *context.Context) {
shared.PerformSecretsPost(
ctx,
sCtx.OwnerID,
sCtx.RepoID,
ctx.Doer,
sCtx.Owner,
sCtx.Repo,
sCtx.RedirectLink,
)
}
@ -120,8 +123,9 @@ func SecretsDelete(ctx *context.Context) {
}
shared.PerformSecretsDelete(
ctx,
sCtx.OwnerID,
sCtx.RepoID,
ctx.Doer,
sCtx.Owner,
sCtx.Repo,
sCtx.RedirectLink,
)
}

View File

@ -33,6 +33,7 @@ import (
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
@ -182,6 +183,11 @@ func SettingsPost(ctx *context.Context) {
ctx.ServerError("UpdateRepository", err)
return
}
if visibilityChanged {
audit.RecordRepositoryVisibility(ctx, ctx.Doer, repo)
}
log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
@ -371,6 +377,8 @@ func SettingsPost(ctx *context.Context) {
return
}
audit.RecordRepositoryMirrorPushRemove(ctx, ctx.Doer, repo, m)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
@ -434,6 +442,8 @@ func SettingsPost(ctx *context.Context) {
return
}
audit.RecordRepositoryMirrorPushAdd(ctx, ctx.Doer, repo, m)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
@ -619,6 +629,7 @@ func SettingsPost(ctx *context.Context) {
return
}
}
log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
@ -637,6 +648,8 @@ func SettingsPost(ctx *context.Context) {
ctx.ServerError("UpdateRepository", err)
return
}
audit.RecordRepositorySigningVerification(ctx, ctx.Doer, repo)
}
log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@ -714,6 +727,9 @@ func SettingsPost(ctx *context.Context) {
ctx.ServerError("DeleteMirrorByRepoID", err)
return
}
audit.RecordRepositoryConvertMirror(ctx, ctx.Doer, repo)
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
ctx.Redirect(repo.Link())
@ -745,7 +761,7 @@ func SettingsPost(ctx *context.Context) {
return
}
if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
if err := repo_service.ConvertForkToNormalRepository(ctx, ctx.Doer, repo); err != nil {
log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
ctx.ServerError("Convert Fork", err)
return
@ -798,7 +814,7 @@ func SettingsPost(ctx *context.Context) {
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
} else {
ctx.ServerError("TransferOwnership", err)
ctx.ServerError("StartRepositoryTransfer", err)
}
return
@ -841,6 +857,8 @@ func SettingsPost(ctx *context.Context) {
return
}
audit.RecordRepositoryTransferCancel(ctx, ctx.Doer, ctx.Repo.Repository)
log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
ctx.Redirect(repo.Link() + "/settings")
@ -879,10 +897,11 @@ func SettingsPost(ctx *context.Context) {
return
}
err := wiki_service.DeleteWiki(ctx, repo)
if err != nil {
log.Error("Delete Wiki: %v", err.Error())
if err := wiki_service.DeleteWiki(ctx, ctx.Doer, repo); err != nil {
ctx.ServerError("DeleteWiki", err)
return
}
log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
@ -911,6 +930,8 @@ func SettingsPost(ctx *context.Context) {
log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
audit.RecordRepositoryArchive(ctx, ctx.Doer, repo)
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@ -935,6 +956,8 @@ func SettingsPost(ctx *context.Context) {
}
}
audit.RecordRepositoryUnarchive(ctx, ctx.Doer, repo)
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
@ -25,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
@ -58,8 +60,8 @@ func Webhooks(ctx *context.Context) {
}
type ownerRepoCtx struct {
OwnerID int64
RepoID int64
Owner *user_model.User
Repo *repo_model.Repository
IsAdmin bool
IsSystemWebhook bool
Link string
@ -71,7 +73,7 @@ type ownerRepoCtx struct {
func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &ownerRepoCtx{
RepoID: ctx.Repo.Repository.ID,
Repo: ctx.Repo.Repository,
Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
NewTemplate: tplHookNew,
@ -80,7 +82,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
if ctx.Data["PageIsOrgSettings"] == true {
return &ownerRepoCtx{
OwnerID: ctx.ContextUser.ID,
Owner: ctx.ContextUser,
Link: path.Join(ctx.Org.OrgLink, "settings/hooks"),
LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"),
NewTemplate: tplOrgHookNew,
@ -89,7 +91,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
if ctx.Data["PageIsUserSettings"] == true {
return &ownerRepoCtx{
OwnerID: ctx.Doer.ID,
Owner: ctx.Doer,
Link: path.Join(setting.AppSubURL, "/user/settings/hooks"),
LinkNew: path.Join(setting.AppSubURL, "/user/settings/hooks"),
NewTemplate: tplUserHookNew,
@ -229,8 +231,17 @@ func createWebhook(ctx *context.Context, params webhookParams) {
}
}
repoID := int64(0)
if orCtx.Repo != nil {
repoID = orCtx.Repo.ID
}
ownerID := int64(0)
if orCtx.Owner != nil {
ownerID = orCtx.Owner.ID
}
w := &webhook.Webhook{
RepoID: orCtx.RepoID,
RepoID: repoID,
URL: params.URL,
HTTPMethod: params.HTTPMethod,
ContentType: params.ContentType,
@ -239,7 +250,7 @@ func createWebhook(ctx *context.Context, params webhookParams) {
IsActive: params.WebhookForm.Active,
Type: params.Type,
Meta: string(meta),
OwnerID: orCtx.OwnerID,
OwnerID: ownerID,
IsSystemWebhook: orCtx.IsSystemWebhook,
}
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
@ -255,6 +266,8 @@ func createWebhook(ctx *context.Context, params webhookParams) {
return
}
audit.RecordWebhookAdd(ctx, ctx.Doer, orCtx.Owner, orCtx.Repo, w)
ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
ctx.Redirect(orCtx.Link)
}
@ -307,6 +320,8 @@ func editWebhook(ctx *context.Context, params webhookParams) {
return
}
audit.RecordWebhookUpdate(ctx, ctx.Doer, orCtx.Owner, orCtx.Repo, w)
ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
}
@ -591,10 +606,10 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
ctx.Data["BaseLinkNew"] = orCtx.LinkNew
var w *webhook.Webhook
if orCtx.RepoID > 0 {
w, err = webhook.GetWebhookByRepoID(ctx, orCtx.RepoID, ctx.ParamsInt64(":id"))
} else if orCtx.OwnerID > 0 {
w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.OwnerID, ctx.ParamsInt64(":id"))
if orCtx.Repo != nil {
w, err = webhook.GetWebhookByRepoID(ctx, orCtx.Repo.ID, ctx.ParamsInt64(":id"))
} else if orCtx.Owner != nil {
w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.Owner.ID, ctx.ParamsInt64(":id"))
} else if orCtx.IsAdmin {
w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.ParamsInt64(":id"))
}
@ -728,9 +743,17 @@ func ReplayWebhook(ctx *context.Context) {
// DeleteWebhook delete a webhook
func DeleteWebhook(ctx *context.Context) {
hook, err := webhook.GetWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id"))
if err != nil {
ctx.ServerError("GetWebhookByRepoID", err)
return
}
if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
} else {
audit.RecordWebhookRemove(ctx, ctx.Doer, nil, ctx.Repo.Repository, hook)
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}

View File

@ -5,7 +5,9 @@ package secrets
import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@ -14,7 +16,16 @@ import (
secret_service "code.gitea.io/gitea/services/secrets"
)
func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
func SetSecretsContext(ctx *context.Context, owner *user_model.User, repo *repo_model.Repository) {
ownerID := int64(0)
if owner != nil {
ownerID = owner.ID
}
repoID := int64(0)
if repo != nil {
repoID = repo.ID
}
secrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: ownerID, RepoID: repoID})
if err != nil {
ctx.ServerError("FindSecrets", err)
@ -24,10 +35,10 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
ctx.Data["Secrets"] = secrets
}
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
func PerformSecretsPost(ctx *context.Context, doer, owner *user_model.User, repo *repo_model.Repository, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
s, _, err := secret_service.CreateOrUpdateSecret(ctx, doer, owner, repo, form.Name, util.ReserveLineBreakForTextarea(form.Data))
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
@ -38,10 +49,10 @@ func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL
ctx.JSONRedirect(redirectURL)
}
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
func PerformSecretsDelete(ctx *context.Context, doer, owner *user_model.User, repo *repo_model.Repository, redirectURL string) {
id := ctx.FormInt64("id")
err := secret_service.DeleteSecretByID(ctx, ownerID, repoID, id)
err := secret_service.DeleteSecretByID(ctx, doer, owner, repo, id)
if err != nil {
log.Error("DeleteSecretByID(%d) failed: %v", id, err)
ctx.JSONError(ctx.Tr("secrets.deletion.failed"))

View File

@ -65,7 +65,7 @@ func AccountPost(ctx *context.Context) {
Password: optional.Some(form.Password),
MustChangePassword: optional.Some(false),
}
if err := user.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
if err := user.UpdateAuth(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
@ -165,7 +165,7 @@ func EmailPost(ctx *context.Context) {
opts := &user.UpdateOptions{
EmailNotificationsPreference: optional.Some(preference),
}
if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil {
if err := user.UpdateUser(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
log.Error("Set Email Notifications failed: %v", err)
ctx.ServerError("UpdateUser", err)
return
@ -183,7 +183,7 @@ func EmailPost(ctx *context.Context) {
return
}
if err := user.AddEmailAddresses(ctx, ctx.Doer, []string{form.Email}); err != nil {
if err := user.AddEmailAddresses(ctx, ctx.Doer, ctx.Doer, []string{form.Email}); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
loadAccountData(ctx)
@ -210,7 +210,6 @@ func EmailPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
log.Trace("Email address added: %s", form.Email)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
@ -222,11 +221,10 @@ func DeleteEmail(ctx *context.Context) {
return
}
if err := user.DeleteEmailAddresses(ctx, ctx.Doer, []string{email.Email}); err != nil {
if err := user.DeleteEmailAddresses(ctx, ctx.Doer, ctx.Doer, []string{email.Email}); err != nil {
ctx.ServerError("DeleteEmailAddresses", err)
return
}
log.Trace("Email address deleted: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/account")
@ -273,7 +271,7 @@ func DeleteAccount(ctx *context.Context) {
return
}
if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
if err := user.DeleteUser(ctx, ctx.Doer, ctx.Doer, false); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@ -70,6 +71,8 @@ func ApplicationsPost(ctx *context.Context) {
return
}
audit.RecordUserAccessTokenAdd(ctx, ctx.Doer, ctx.Doer, t)
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
ctx.Flash.Info(t.Token)
@ -78,9 +81,17 @@ 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.ServerError("GetAccessTokenByID", err)
return
}
if err := auth_model.DeleteAccessTokenByID(ctx, t.ID, ctx.Doer.ID); err != nil {
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
audit.RecordUserAccessTokenRemove(ctx, ctx.Doer, ctx.Doer, t)
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
}

View File

@ -0,0 +1,55 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
audit_model "code.gitea.io/gitea/models/audit"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
const (
tplAuditLogs base.TplName = "user/settings/audit_logs"
)
func ViewAuditLogs(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor.audit.title")
ctx.Data["PageIsSettingsAudit"] = true
page := ctx.FormInt("page")
if page < 1 {
page = 1
}
opts := &audit_model.EventSearchOptions{
Sort: ctx.FormString("sort"),
ScopeType: audit_model.TypeUser,
ScopeID: ctx.Doer.ID,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.Admin.NoticePagingNum,
},
}
ctx.Data["AuditSort"] = opts.Sort
evs, total, err := audit.FindEvents(ctx, opts)
if err != nil {
ctx.ServerError("", err)
return
}
ctx.Data["AuditEvents"] = evs
pager := context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
pager.AddParamString("sort", opts.Sort)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplAuditLogs)
}

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@ -63,7 +64,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):
@ -76,6 +78,9 @@ func KeysPost(ctx *context.Context) {
}
return
}
audit.RecordUserKeyPrincipalAdd(ctx, ctx.Doer, ctx.Doer, key)
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
@ -124,6 +129,11 @@ func KeysPost(ctx *context.Context) {
}
return
}
for _, key := range keys {
audit.RecordUserKeyGPGAdd(ctx, ctx.Doer, ctx.Doer, key)
}
keyIDs := ""
for _, key := range keys {
keyIDs += key.KeyID
@ -180,7 +190,8 @@ func KeysPost(ctx *context.Context) {
return
}
if _, err = asymkey_model.AddPublicKey(ctx, ctx.Doer.ID, form.Title, content, 0); err != nil {
key, err := asymkey_model.AddPublicKey(ctx, ctx.Doer.ID, form.Title, content, 0)
if err != nil {
ctx.Data["HasSSHError"] = true
switch {
case asymkey_model.IsErrKeyAlreadyExist(err):
@ -201,6 +212,9 @@ func KeysPost(ctx *context.Context) {
}
return
}
audit.RecordUserKeySSHAdd(ctx, ctx.Doer, ctx.Doer, key)
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
@ -245,10 +259,23 @@ func DeleteKey(ctx *context.Context) {
ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
return
}
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
} else {
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 key != nil {
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, key.ID); err != nil {
ctx.ServerError("DeleteGPGKey", err)
return
}
audit.RecordUserKeyGPGRemove(ctx, ctx.Doer, ctx.Doer, key)
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
} else {
ctx.Flash.Error(ctx.Tr("error.occurred"))
}
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
@ -259,7 +286,7 @@ func DeleteKey(ctx *context.Context) {
keyID := ctx.FormInt64("id")
external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
if err != nil {
ctx.ServerError("sshKeysExternalManaged", err)
ctx.ServerError("PublicKeyIsExternallyManaged", err)
return
}
if external {

View File

@ -4,6 +4,7 @@
package setting
import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
@ -13,9 +14,9 @@ const (
tplSettingsOAuthApplicationEdit base.TplName = "user/settings/applications_oauth2_edit"
)
func newOAuth2CommonHandlers(userID int64) *OAuth2CommonHandlers {
func newOAuth2CommonHandlers(u *user_model.User) *OAuth2CommonHandlers {
return &OAuth2CommonHandlers{
OwnerID: userID,
Owner: u,
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")
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")
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")
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)
}

View File

@ -4,23 +4,34 @@
package setting
import (
"errors"
"fmt"
"net/http"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
type OAuth2CommonHandlers struct {
OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID
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 base.TplName // the template for the application edit page
Doer *user_model.User
Owner *user_model.User // nil for instance-wide, otherwise Org or User
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 base.TplName // the template for the application edit page
}
func (oa *OAuth2CommonHandlers) ownerID() int64 {
if oa.Owner != nil {
return oa.Owner.ID
}
return 0
}
func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) {
@ -51,7 +62,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,
})
if err != nil {
@ -59,6 +70,8 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
return
}
audit.RecordOAuth2ApplicationAdd(ctx, oa.Doer, oa.Owner, app)
// render the edit page with secret
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"), true)
ctx.Data["App"] = app
@ -82,7 +95,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("Application not found", nil)
return
}
@ -100,17 +113,22 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
}
// TODO validate redirect URI
var err error
if ctx.Data["App"], err = auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
app, err := auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
ID: ctx.ParamsInt64("id"),
Name: form.Name,
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID,
UserID: oa.ownerID(),
ConfidentialClient: form.ConfidentialClient,
}); err != nil {
})
if err != nil {
ctx.ServerError("UpdateOAuth2Application", err)
return
}
ctx.Data["App"] = app
audit.RecordOAuth2ApplicationUpdate(ctx, oa.Doer, oa.Owner, app)
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
ctx.Redirect(oa.BasePathList)
}
@ -126,7 +144,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("Application not found", nil)
return
}
@ -136,28 +154,65 @@ func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) {
ctx.ServerError("GenerateClientSecret", err)
return
}
audit.RecordOAuth2ApplicationSecret(ctx, oa.Doer, oa.Owner, app)
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"), true)
oa.renderEditPage(ctx)
}
// DeleteApp deletes the given oauth2 application
func (oa *OAuth2CommonHandlers) DeleteApp(ctx *context.Context) {
if err := auth.DeleteOAuth2Application(ctx, ctx.ParamsInt64("id"), oa.OwnerID); err != nil {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound("Application not found", err)
} else {
ctx.ServerError("GetOAuth2ApplicationByID", err)
}
return
}
if err := auth.DeleteOAuth2Application(ctx, app.ID, oa.ownerID()); err != nil {
ctx.ServerError("DeleteOAuth2Application", err)
return
}
audit.RecordOAuth2ApplicationRemove(ctx, oa.Doer, oa.Owner, app)
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.ParamsInt64("grantId"), oa.OwnerID); err != nil {
grant, err := auth.GetOAuth2GrantByID(ctx, ctx.ParamsInt64("grantId"))
if err != nil {
ctx.ServerError("GetOAuth2GrantByID", err)
return
}
if grant == nil {
ctx.NotFound("Grant not found", nil)
return
}
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound("Application not found", err)
} else {
ctx.ServerError("GetOAuth2ApplicationByID", err)
}
return
}
if err := auth.RevokeOAuth2Grant(ctx, grant.ID, oa.ownerID()); err != nil {
ctx.ServerError("RevokeOAuth2Grant", err)
return
}
audit.RecordUserOAuth2ApplicationRevoke(ctx, oa.Doer, oa.Owner, app, grant)
ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
ctx.JSONRedirect(oa.BasePathList)
}

View File

@ -66,7 +66,7 @@ func ProfilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
if form.Name != "" {
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name); err != nil {
if err := user_service.RenameUser(ctx, ctx.Doer, ctx.Doer, form.Name); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
@ -96,12 +96,11 @@ func ProfilePost(ctx *context.Context) {
Visibility: optional.Some(form.Visibility),
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
@ -363,7 +362,7 @@ func UpdateUIThemePost(ctx *context.Context) {
opts := &user_service.UpdateOptions{
Theme: optional.Some(form.Theme),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
} else {
ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
@ -389,7 +388,7 @@ func UpdateUserLang(ctx *context.Context) {
opts := &user_service.UpdateOptions{
Language: optional.Some(form.Language),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
if err := user_service.UpdateUser(ctx, ctx.Doer, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -49,6 +50,8 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
return
}
audit.RecordUserTwoFactorRegenerate(ctx, ctx.Doer, ctx.Doer, t)
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
@ -78,6 +81,8 @@ func DisableTwoFactor(ctx *context.Context) {
return
}
audit.RecordUserTwoFactorDisable(ctx, ctx.Doer, ctx.Doer, t)
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
@ -244,6 +249,8 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return
}
audit.RecordUserTwoFactorEnable(ctx, ctx.Doer, ctx.Doer)
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@ -97,6 +98,9 @@ func settingsOpenIDVerify(ctx *context.Context) {
ctx.ServerError("AddUserOpenID", err)
return
}
audit.RecordUserOpenIDAdd(ctx, ctx.Doer, ctx.Doer, oid)
log.Trace("Associated OpenID %s to user %s", id, ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
@ -105,10 +109,19 @@ func settingsOpenIDVerify(ctx *context.Context) {
// DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) {
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
oid, err := user_model.GetUserOpenID(ctx, ctx.FormInt64("id"), ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenID", err)
return
}
if err := user_model.DeleteUserOpenID(ctx, oid); err != nil {
ctx.ServerError("DeleteUserOpenID", err)
return
}
audit.RecordUserOpenIDRemove(ctx, ctx.Doer, ctx.Doer, oid)
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
)
@ -40,17 +41,24 @@ func Security(ctx *context.Context) {
// DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) {
id := ctx.FormInt64("id")
if id <= 0 {
ctx.Flash.Error("Account link id is not given")
} else {
if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, id); err != nil {
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
user := &user_model.ExternalLoginUser{UserID: ctx.Doer.ID, LoginSourceID: ctx.FormInt64("id")}
if has, err := user_model.GetExternalLogin(ctx, user); err != nil || !has {
if !has {
err = user_model.ErrExternalLoginUserNotExist{UserID: user.UserID, LoginSourceID: user.LoginSourceID}
}
ctx.ServerError("RemoveAccountLink", err)
return
}
if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, user.LoginSourceID); err != nil {
ctx.ServerError("RemoveAccountLink", err)
return
}
audit.RecordUserExternalLoginRemove(ctx, ctx.Doer, ctx.Doer, user)
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -99,22 +100,34 @@ 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")
audit.RecordUserWebAuthAdd(ctx, ctx.Doer, ctx.Doer, dbCred)
ctx.JSON(http.StatusCreated, cred)
}
// WebauthnDelete deletes an security key by id
func WebauthnDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.WebauthnDeleteForm)
if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil {
cred, err := auth.GetWebAuthnCredentialByID(ctx, form.ID)
if err != nil {
ctx.ServerError("GetWebAuthnCredentialByID", err)
return
}
if ok, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("DeleteCredential", err)
return
} else if ok {
audit.RecordUserWebAuthRemove(ctx, ctx.Doer, ctx.Doer, cred)
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/audit"
"code.gitea.io/gitea/services/context"
)
@ -37,9 +38,17 @@ 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.ServerError("GetWebhookByOwnerID", err)
return
}
if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Doer.ID, hook.ID); err != nil {
ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error())
} else {
audit.RecordWebhookRemove(ctx, ctx.Doer, ctx.Doer, nil, hook)
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}

View File

@ -380,6 +380,13 @@ func registerRoutes(m *web.Route) {
}
}
auditLogsEnabled := func(ctx *context.Context) {
if !setting.Audit.Enabled {
ctx.Error(http.StatusNotFound)
return
}
}
reqUnitAccess := func(unitType unit.Type, accessMode perm.AccessMode, ignoreGlobal bool) func(ctx *context.Context) {
return func(ctx *context.Context) {
// only check global disabled units when ignoreGlobal is false
@ -649,6 +656,8 @@ func registerRoutes(m *web.Route) {
addWebhookEditRoutes()
}, webhooksEnabled)
m.Get("/audit_logs", auditLogsEnabled, user_setting.ViewAuditLogs)
m.Group("/blocked_users", func() {
m.Get("", user_setting.BlockedUsers)
m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
@ -696,6 +705,7 @@ func registerRoutes(m *web.Route) {
})
m.Group("/monitor", func() {
m.Get("/audit_logs", auditLogsEnabled, admin.ViewAuditLogs)
m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks)
m.Get("/stacktrace", admin.Stacktrace)
@ -933,6 +943,8 @@ func registerRoutes(m *web.Route) {
addSettingsVariablesRoutes()
}, actions.MustEnableActions)
m.Get("/audit_logs", auditLogsEnabled, org_setting.ViewAuditLogs)
m.Methods("GET,POST", "/delete", org.SettingsDelete)
m.Group("/packages", func() {
@ -1111,6 +1123,7 @@ func registerRoutes(m *web.Route) {
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
}, actions.MustEnableActions)
m.Get("/audit_logs", auditLogsEnabled, 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)

View File

@ -5,10 +5,16 @@ package asymkey
import (
"context"
"errors"
"fmt"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/audit"
)
// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
@ -19,6 +25,16 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error
}
defer committer.Close()
key, err := asymkey_model.GetDeployKeyByID(dbCtx, id)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return fmt.Errorf("GetDeployKeyByID: %w", err)
}
repo, err := repo_model.GetRepositoryByID(dbCtx, key.RepoID)
if err != nil {
return fmt.Errorf("GetRepositoryByID: %w", err)
}
if err := models.DeleteDeployKey(dbCtx, doer, id); err != nil {
return err
}
@ -26,5 +42,7 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error
return err
}
audit.RecordRepositoryDeployKeyRemove(ctx, doer, repo, key)
return RewriteAllPublicKeys(ctx)
}

View File

@ -9,6 +9,7 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/audit"
)
// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
@ -18,6 +19,11 @@ func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err
return err
}
owner, err := user_model.GetUserByID(db.DefaultContext, key.OwnerID)
if err != nil {
return err
}
// Check if user has access to delete this key.
if !doer.IsAdmin && doer.ID != key.OwnerID {
return asymkey_model.ErrKeyAccessDenied{
@ -43,8 +49,12 @@ func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err
committer.Close()
if key.Type == asymkey_model.KeyTypePrincipal {
audit.RecordUserKeyPrincipalRemove(ctx, doer, owner, key)
return RewriteAllPrincipalKeys(ctx)
}
audit.RecordUserKeySSHRemove(ctx, doer, owner, key)
return RewriteAllPublicKeys(ctx)
}

153
services/audit/audit.go Normal file
View File

@ -0,0 +1,153 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"fmt"
asymkey_model "code.gitea.io/gitea/models/asymkey"
audit_model "code.gitea.io/gitea/models/audit"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
organization_model "code.gitea.io/gitea/models/organization"
repository_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
)
type TypeDescriptor struct {
Type audit_model.ObjectType `json:"type"`
ID int64 `json:"id"`
Object any `json:"-"`
}
func (d TypeDescriptor) DisplayName() string {
switch t := d.Object.(type) {
case *repository_model.Repository:
return t.FullName()
case *user_model.User:
return t.Name
case *organization_model.Organization:
return t.Name
case *user_model.EmailAddress:
return t.Email
case *organization_model.Team:
return t.Name
case *auth_model.WebAuthnCredential:
return t.Name
case *user_model.UserOpenID:
return t.URI
case *auth_model.AccessToken:
return t.Name
case *auth_model.OAuth2Application:
return t.Name
case *auth_model.Source:
return t.Name
case *asymkey_model.PublicKey:
return t.Fingerprint
case *asymkey_model.GPGKey:
return t.KeyID
case *secret_model.Secret:
return t.Name
case *webhook_model.Webhook:
return t.URL
case *git_model.ProtectedTag:
return t.NamePattern
case *git_model.ProtectedBranch:
return t.RuleName
case *repository_model.PushMirror:
return t.RemoteAddress
}
if d.Type == audit_model.TypeSystem {
return "System"
}
return ""
}
func (d TypeDescriptor) HTMLURL() string {
switch t := d.Object.(type) {
case *repository_model.Repository:
return t.HTMLURL()
case *user_model.User:
return t.HTMLURL()
case *organization_model.Organization:
return t.HTMLURL()
}
return ""
}
func Init() error {
if !setting.Audit.Enabled {
return nil
}
return initAuditFile()
}
var systemObject struct{}
func scopeToDescription(scope any) TypeDescriptor {
if scope == &systemObject {
return TypeDescriptor{audit_model.TypeSystem, 0, nil}
}
switch s := scope.(type) {
case *repository_model.Repository, *user_model.User, *organization_model.Organization:
return typeToDescription(scope)
default:
panic(fmt.Sprintf("unsupported scope type: %T", s))
}
}
func typeToDescription(val any) TypeDescriptor {
if val == &systemObject {
return TypeDescriptor{audit_model.TypeSystem, 0, nil}
}
switch t := val.(type) {
case *repository_model.Repository:
return TypeDescriptor{audit_model.TypeRepository, t.ID, val}
case *user_model.User:
if t.IsOrganization() {
return TypeDescriptor{audit_model.TypeOrganization, t.ID, val}
}
return TypeDescriptor{audit_model.TypeUser, t.ID, val}
case *organization_model.Organization:
return TypeDescriptor{audit_model.TypeOrganization, t.ID, val}
case *user_model.EmailAddress:
return TypeDescriptor{audit_model.TypeEmailAddress, t.ID, val}
case *organization_model.Team:
return TypeDescriptor{audit_model.TypeTeam, t.ID, val}
case *auth_model.WebAuthnCredential:
return TypeDescriptor{audit_model.TypeWebAuthnCredential, t.ID, val}
case *user_model.UserOpenID:
return TypeDescriptor{audit_model.TypeOpenID, t.ID, val}
case *auth_model.AccessToken:
return TypeDescriptor{audit_model.TypeAccessToken, t.ID, val}
case *auth_model.OAuth2Application:
return TypeDescriptor{audit_model.TypeOAuth2Application, t.ID, val}
case *auth_model.Source:
return TypeDescriptor{audit_model.TypeAuthenticationSource, t.ID, val}
case *asymkey_model.PublicKey:
return TypeDescriptor{audit_model.TypePublicKey, t.ID, val}
case *asymkey_model.GPGKey:
return TypeDescriptor{audit_model.TypeGPGKey, t.ID, val}
case *secret_model.Secret:
return TypeDescriptor{audit_model.TypeSecret, t.ID, val}
case *webhook_model.Webhook:
return TypeDescriptor{audit_model.TypeWebhook, t.ID, val}
case *git_model.ProtectedTag:
return TypeDescriptor{audit_model.TypeProtectedTag, t.ID, val}
case *git_model.ProtectedBranch:
return TypeDescriptor{audit_model.TypeProtectedBranch, t.ID, val}
case *repository_model.PushMirror:
return TypeDescriptor{audit_model.TypePushMirror, t.ID, val}
default:
panic(fmt.Sprintf("unsupported type: %T", t))
}
}

View File

@ -0,0 +1,307 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"context"
"net/http"
"testing"
"time"
asymkey_model "code.gitea.io/gitea/models/asymkey"
audit_model "code.gitea.io/gitea/models/audit"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
organization_model "code.gitea.io/gitea/models/organization"
repository_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestBuildEvent(t *testing.T) {
equal := func(expected, e *Event) {
expected.Time = time.Time{}
e.Time = time.Time{}
assert.Equal(t, expected, e)
}
ctx := context.Background()
u := &user_model.User{ID: 1, Name: "TestUser"}
r := &repository_model.Repository{ID: 3, Name: "TestRepo", OwnerName: "TestUser"}
m := &repository_model.PushMirror{ID: 4}
doer := &user_model.User{ID: 2, Name: "Doer"}
equal(
&Event{
Action: audit_model.UserCreate,
Actor: TypeDescriptor{Type: "user", ID: 2, Object: doer},
Scope: TypeDescriptor{Type: "user", ID: 1, Object: u},
Target: TypeDescriptor{Type: "user", ID: 1, Object: u},
Message: "Created user TestUser.",
},
buildEvent(
ctx,
audit_model.UserCreate,
doer,
u,
u,
"Created user %s.",
u.Name,
),
)
equal(
&Event{
Action: audit_model.RepositoryMirrorPushAdd,
Actor: TypeDescriptor{Type: "user", ID: 2, Object: doer},
Scope: TypeDescriptor{Type: "repository", ID: 3, Object: r},
Target: TypeDescriptor{Type: "push_mirror", ID: 4, Object: m},
Message: "Added push mirror for repository TestUser/TestRepo.",
},
buildEvent(
ctx,
audit_model.RepositoryMirrorPushAdd,
doer,
r,
m,
"Added push mirror for repository %s.",
r.FullName(),
),
)
e := buildEvent(ctx, audit_model.UserCreate, doer, u, u, "")
assert.Empty(t, e.IPAddress)
ctx = context.WithValue(ctx, httplib.RequestContextKey, &http.Request{RemoteAddr: "127.0.0.1:1234"})
e = buildEvent(ctx, audit_model.UserCreate, doer, u, u, "")
assert.Equal(t, "127.0.0.1", e.IPAddress)
}
func TestScopeToDescription(t *testing.T) {
cases := []struct {
ShouldPanic bool
Scope any
Expected TypeDescriptor
}{
{
Scope: nil,
ShouldPanic: true,
},
{
Scope: &systemObject,
Expected: TypeDescriptor{Type: audit_model.TypeSystem, ID: 0},
},
{
Scope: &user_model.User{ID: 1, Name: "TestUser"},
Expected: TypeDescriptor{Type: audit_model.TypeUser, ID: 1},
},
{
Scope: &organization_model.Organization{ID: 2, Name: "TestOrg"},
Expected: TypeDescriptor{Type: audit_model.TypeOrganization, ID: 2},
},
{
Scope: &repository_model.Repository{ID: 3, Name: "TestRepo", OwnerName: "TestUser"},
Expected: TypeDescriptor{Type: audit_model.TypeRepository, ID: 3},
},
{
ShouldPanic: true,
Scope: &organization_model.Team{ID: 345, Name: "Team"},
},
{
ShouldPanic: true,
Scope: 1234,
},
}
for _, c := range cases {
if c.Scope != &systemObject {
c.Expected.Object = c.Scope
}
if c.ShouldPanic {
assert.Panics(t, func() {
_ = scopeToDescription(c.Scope)
})
} else {
assert.Equal(t, c.Expected, scopeToDescription(c.Scope), "Unexpected descriptor for scope: %T", c.Scope)
}
}
}
func TestTypeToDescription(t *testing.T) {
setting.AppURL = "http://localhost:3000/"
type Expected struct {
TypeDescriptor TypeDescriptor
DisplayName string
HTMLURL string
}
cases := []struct {
ShouldPanic bool
Type any
Expected Expected
}{
{
Type: nil,
ShouldPanic: true,
},
{
Type: &systemObject,
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeSystem, ID: 0},
DisplayName: "System",
},
},
{
Type: &user_model.User{ID: 1, Name: "TestUser"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeUser, ID: 1},
DisplayName: "TestUser",
HTMLURL: "http://localhost:3000/TestUser",
},
},
{
Type: &organization_model.Organization{ID: 2, Name: "TestOrg"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeOrganization, ID: 2},
DisplayName: "TestOrg",
HTMLURL: "http://localhost:3000/TestOrg",
},
},
{
Type: &user_model.EmailAddress{ID: 3, Email: "user@gitea.com"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeEmailAddress, ID: 3},
DisplayName: "user@gitea.com",
},
},
{
Type: &repository_model.Repository{ID: 3, Name: "TestRepo", OwnerName: "TestUser"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeRepository, ID: 3},
DisplayName: "TestUser/TestRepo",
HTMLURL: "http://localhost:3000/TestUser/TestRepo",
},
},
{
Type: &organization_model.Team{ID: 4, Name: "TestTeam"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeTeam, ID: 4},
DisplayName: "TestTeam",
},
},
{
Type: &auth_model.WebAuthnCredential{ID: 6, Name: "TestCredential"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeWebAuthnCredential, ID: 6},
DisplayName: "TestCredential",
},
},
{
Type: &user_model.UserOpenID{ID: 7, URI: "test://uri"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeOpenID, ID: 7},
DisplayName: "test://uri",
},
},
{
Type: &auth_model.AccessToken{ID: 8, Name: "TestToken"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeAccessToken, ID: 8},
DisplayName: "TestToken",
},
},
{
Type: &auth_model.OAuth2Application{ID: 9, Name: "TestOAuth2Application"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeOAuth2Application, ID: 9},
DisplayName: "TestOAuth2Application",
},
},
{
Type: &auth_model.Source{ID: 11, Name: "TestSource"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeAuthenticationSource, ID: 11},
DisplayName: "TestSource",
},
},
{
Type: &asymkey_model.PublicKey{ID: 13, Fingerprint: "TestPublicKey"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypePublicKey, ID: 13},
DisplayName: "TestPublicKey",
},
},
{
Type: &asymkey_model.GPGKey{ID: 14, KeyID: "TestGPGKey"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeGPGKey, ID: 14},
DisplayName: "TestGPGKey",
},
},
{
Type: &secret_model.Secret{ID: 15, Name: "TestSecret"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeSecret, ID: 15},
DisplayName: "TestSecret",
},
},
{
Type: &webhook_model.Webhook{ID: 16, URL: "test://webhook"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeWebhook, ID: 16},
DisplayName: "test://webhook",
},
},
{
Type: &git_model.ProtectedTag{ID: 17, NamePattern: "TestProtectedTag"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeProtectedTag, ID: 17},
DisplayName: "TestProtectedTag",
},
},
{
Type: &git_model.ProtectedBranch{ID: 18, RuleName: "TestProtectedBranch"},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypeProtectedBranch, ID: 18},
DisplayName: "TestProtectedBranch",
},
},
{
Type: &repository_model.PushMirror{ID: 19},
Expected: Expected{
TypeDescriptor: TypeDescriptor{Type: audit_model.TypePushMirror, ID: 19},
DisplayName: "",
},
},
{
ShouldPanic: true,
Type: 1234,
},
}
for _, c := range cases {
if c.Type != &systemObject {
c.Expected.TypeDescriptor.Object = c.Type
}
if c.ShouldPanic {
assert.Panics(t, func() {
_ = typeToDescription(c.Type)
})
} else {
d := typeToDescription(c.Type)
assert.Equal(t, c.Expected.TypeDescriptor, d, "Unexpected descriptor for type: %T", c.Type)
assert.Equal(t, c.Expected.DisplayName, d.DisplayName(), "Unexpected display name for type: %T", c.Type)
assert.Equal(t, c.Expected.HTMLURL, d.HTMLURL(), "Unexpected url for type: %T", c.Type)
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"context"
audit_model "code.gitea.io/gitea/models/audit"
"code.gitea.io/gitea/modules/timeutil"
)
func writeToDatabase(ctx context.Context, e *Event) error {
_, err := audit_model.InsertEvent(ctx, &audit_model.Event{
Action: e.Action,
ActorID: e.Actor.ID,
ScopeType: e.Scope.Type,
ScopeID: e.Scope.ID,
TargetType: e.Target.Type,
TargetID: e.Target.ID,
Message: e.Message,
IPAddress: e.IPAddress,
TimestampUnix: timeutil.TimeStamp(e.Time.Unix()),
})
return err
}

134
services/audit/display.go Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"context"
"fmt"
asymkey_model "code.gitea.io/gitea/models/asymkey"
audit_model "code.gitea.io/gitea/models/audit"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
organization_model "code.gitea.io/gitea/models/organization"
repository_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
)
type cache = map[audit_model.ObjectType]map[int64]TypeDescriptor
func FindEvents(ctx context.Context, opts *audit_model.EventSearchOptions) ([]*Event, int64, error) {
events, total, err := audit_model.FindEvents(ctx, opts)
if err != nil {
return nil, 0, err
}
return fromDatabaseEvents(ctx, events), total, nil
}
func fromDatabaseEvents(ctx context.Context, evs []*audit_model.Event) []*Event {
c := cache{}
users := make(map[int64]TypeDescriptor)
for _, systemUser := range []*user_model.User{
user_model.NewGhostUser(),
user_model.NewActionsUser(),
user_model.NewCLIUser(),
user_model.NewAuthenticationSourceUser(),
} {
users[systemUser.ID] = typeToDescription(systemUser)
}
c[audit_model.TypeUser] = users
events := make([]*Event, 0, len(evs))
for _, e := range evs {
events = append(events, fromDatabaseEvent(ctx, e, c))
}
return events
}
func fromDatabaseEvent(ctx context.Context, e *audit_model.Event, c cache) *Event {
return &Event{
Action: e.Action,
Actor: resolveType(ctx, audit_model.TypeUser, e.ActorID, c),
Scope: resolveType(ctx, e.ScopeType, e.ScopeID, c),
Target: resolveType(ctx, e.TargetType, e.TargetID, c),
Message: e.Message,
Time: e.TimestampUnix.AsTime(),
IPAddress: e.IPAddress,
}
}
func resolveType(ctx context.Context, t audit_model.ObjectType, id int64, c cache) TypeDescriptor {
oc, has := c[t]
if !has {
oc = make(map[int64]TypeDescriptor)
c[t] = oc
}
td, has := oc[id]
if has {
return td
}
switch t {
case audit_model.TypeSystem:
td, has = typeToDescription(&systemObject), true
case audit_model.TypeRepository:
td, has = getTypeDescriptorByID[repository_model.Repository](ctx, id)
case audit_model.TypeUser:
td, has = getTypeDescriptorByID[user_model.User](ctx, id)
case audit_model.TypeOrganization:
td, has = getTypeDescriptorByID[organization_model.Organization](ctx, id)
case audit_model.TypeEmailAddress:
td, has = getTypeDescriptorByID[user_model.EmailAddress](ctx, id)
case audit_model.TypeTeam:
td, has = getTypeDescriptorByID[organization_model.Team](ctx, id)
case audit_model.TypeWebAuthnCredential:
td, has = getTypeDescriptorByID[auth_model.WebAuthnCredential](ctx, id)
case audit_model.TypeOpenID:
td, has = getTypeDescriptorByID[user_model.UserOpenID](ctx, id)
case audit_model.TypeAccessToken:
td, has = getTypeDescriptorByID[auth_model.AccessToken](ctx, id)
case audit_model.TypeOAuth2Application:
td, has = getTypeDescriptorByID[auth_model.OAuth2Application](ctx, id)
case audit_model.TypeAuthenticationSource:
td, has = getTypeDescriptorByID[auth_model.Source](ctx, id)
case audit_model.TypePublicKey:
td, has = getTypeDescriptorByID[asymkey_model.PublicKey](ctx, id)
case audit_model.TypeGPGKey:
td, has = getTypeDescriptorByID[asymkey_model.GPGKey](ctx, id)
case audit_model.TypeSecret:
td, has = getTypeDescriptorByID[secret_model.Secret](ctx, id)
case audit_model.TypeWebhook:
td, has = getTypeDescriptorByID[webhook_model.Webhook](ctx, id)
case audit_model.TypeProtectedTag:
td, has = getTypeDescriptorByID[git_model.ProtectedTag](ctx, id)
case audit_model.TypeProtectedBranch:
td, has = getTypeDescriptorByID[git_model.ProtectedBranch](ctx, id)
case audit_model.TypePushMirror:
td, has = getTypeDescriptorByID[repository_model.PushMirror](ctx, id)
default:
panic(fmt.Sprintf("unsupported type: %v", t))
}
if !has {
td = TypeDescriptor{t, id, nil}
}
oc[id] = td
return td
}
func getTypeDescriptorByID[T any](ctx context.Context, id int64) (TypeDescriptor, bool) {
if bean, has, _ := db.GetByID[T](ctx, id); has {
return typeToDescription(bean), true
}
return TypeDescriptor{}, false
}

59
services/audit/file.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"io"
audit_model "code.gitea.io/gitea/models/audit"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util/rotatingfilewriter"
)
var rfw *rotatingfilewriter.RotatingFileWriter
func initAuditFile() error {
if setting.Audit.FileOptions == nil {
return nil
}
opts := setting.Audit.FileOptions
var err error
rfw, err = rotatingfilewriter.Open(opts.FileName, &rotatingfilewriter.Options{
Rotate: opts.LogRotate,
MaximumSize: opts.MaxSize,
RotateDaily: opts.DailyRotate,
KeepDays: opts.MaxDays,
Compress: opts.Compress,
CompressionLevel: opts.CompressionLevel,
})
return err
}
func writeToFile(e *Event) error {
if rfw == nil {
return nil
}
return WriteEventAsJSON(rfw, e)
}
func (d TypeDescriptor) MarshalJSON() ([]byte, error) {
type out struct {
Type audit_model.ObjectType `json:"type"`
ID int64 `json:"id"`
DisplayName string `json:"display_name"`
}
return json.Marshal(out{
Type: d.Type,
ID: d.ID,
DisplayName: d.DisplayName(),
})
}
func WriteEventAsJSON(w io.Writer, e *Event) error {
return json.NewEncoder(w).Encode(e)
}

View File

@ -0,0 +1,46 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"context"
"net/http"
"strings"
"testing"
"time"
audit_model "code.gitea.io/gitea/models/audit"
repository_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httplib"
"github.com/stretchr/testify/assert"
)
func TestWriteEventAsJSON(t *testing.T) {
r := &repository_model.Repository{ID: 3, Name: "TestRepo", OwnerName: "TestUser"}
m := &repository_model.PushMirror{ID: 4}
doer := &user_model.User{ID: 2, Name: "Doer"}
ctx := context.WithValue(context.Background(), httplib.RequestContextKey, &http.Request{RemoteAddr: "127.0.0.1:1234"})
e := buildEvent(
ctx,
audit_model.RepositoryMirrorPushAdd,
doer,
r,
m,
"Added push mirror for repository %s.",
r.FullName(),
)
e.Time = time.Time{}
sb := strings.Builder{}
assert.NoError(t, WriteEventAsJSON(&sb, e))
assert.Equal(
t,
`{"action":"repository:mirror:push:add","actor":{"type":"user","id":2,"display_name":"Doer"},"scope":{"type":"repository","id":3,"display_name":"TestUser/TestRepo"},"target":{"type":"push_mirror","id":4,"display_name":""},"message":"Added push mirror for repository TestUser/TestRepo.","time":"0001-01-01T00:00:00Z","ip_address":"127.0.0.1"}`+"\n",
sb.String(),
)
}

513
services/audit/record.go Normal file
View File

@ -0,0 +1,513 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package audit
import (
"context"
"fmt"
"time"
asymkey_model "code.gitea.io/gitea/models/asymkey"
audit_model "code.gitea.io/gitea/models/audit"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
organization_model "code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm"
repository_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
type Event struct {
Action audit_model.Action `json:"action"`
Actor TypeDescriptor `json:"actor"`
Scope TypeDescriptor `json:"scope"`
Target TypeDescriptor `json:"target"`
Message string `json:"message"`
Time time.Time `json:"time"`
IPAddress string `json:"ip_address"`
}
func buildEvent(ctx context.Context, action audit_model.Action, actor *user_model.User, scope, target any, message string, v ...any) *Event {
return &Event{
Action: action,
Actor: typeToDescription(actor),
Scope: scopeToDescription(scope),
Target: typeToDescription(target),
Message: fmt.Sprintf(message, v...),
Time: time.Now(),
IPAddress: httplib.TryGetIPAddress(ctx),
}
}
func record(ctx context.Context, action audit_model.Action, actor *user_model.User, scope, target any, message string, v ...any) {
if !setting.Audit.Enabled {
return
}
e := buildEvent(ctx, action, actor, scope, target, message, v...)
if err := writeToFile(e); err != nil {
log.Error("Error writing audit event to file: %v", err)
}
if err := writeToDatabase(ctx, e); err != nil {
log.Error("Error writing audit event %+v to database: %v", e, err)
}
}
func RecordUserImpersonation(ctx context.Context, impersonator, target *user_model.User) {
record(ctx, audit_model.UserImpersonation, impersonator, impersonator, target, "User %s impersonating user %s.", impersonator.Name, target.Name)
}
func RecordUserCreate(ctx context.Context, doer, user *user_model.User) {
if user.IsOrganization() {
record(ctx, audit_model.OrganizationCreate, doer, user, user, "Created organization %s.", user.Name)
} else {
record(ctx, audit_model.UserCreate, doer, user, user, "Created user %s.", user.Name)
}
}
func RecordUserDelete(ctx context.Context, doer, user *user_model.User) {
if user.IsOrganization() {
record(ctx, audit_model.OrganizationDelete, doer, user, user, "Deleted organization %s.", user.Name)
} else {
record(ctx, audit_model.UserDelete, doer, user, user, "Deleted user %s.", user.Name)
}
}
func RecordUserAuthenticationFailTwoFactor(ctx context.Context, user *user_model.User) {
record(ctx, audit_model.UserAuthenticationFailTwoFactor, user, user, user, "Failed two-factor authentication for user %s.", user.Name)
}
func RecordUserAuthenticationSource(ctx context.Context, doer, user *user_model.User) {
record(ctx, audit_model.UserAuthenticationSource, doer, user, user, "Changed authentication source of user %s.", user.Name)
}
func RecordUserActive(ctx context.Context, doer, user *user_model.User) {
status := "active"
if !user.IsActive {
status = "inactive"
}
record(ctx, audit_model.UserActive, doer, user, user, "Changed activation status of user %s to %s.", user.Name, status)
}
func RecordUserRestricted(ctx context.Context, doer, user *user_model.User) {
status := "restricted"
if !user.IsRestricted {
status = "unrestricted"
}
record(ctx, audit_model.UserRestricted, doer, user, user, "Changed restricted status of user %s to %s.", user.Name, status)
}
func RecordUserAdmin(ctx context.Context, doer, user *user_model.User) {
status := "admin"
if !user.IsAdmin {
status = "normal user"
}
record(ctx, audit_model.UserAdmin, doer, user, user, "Changed admin status of user %s to %s.", user.Name, status)
}
func RecordUserName(ctx context.Context, doer, user *user_model.User) {
if user.IsOrganization() {
record(ctx, audit_model.OrganizationName, doer, user, user, "Changed organization name to %s.", user.Name)
} else {
record(ctx, audit_model.UserName, doer, user, user, "Changed user name to %s.", user.Name)
}
}
func RecordUserPassword(ctx context.Context, doer, user *user_model.User) {
record(ctx, audit_model.UserPassword, doer, user, user, "Changed password of user %s.", user.Name)
}
func RecordUserPasswordResetRequest(ctx context.Context, doer, user *user_model.User) {
record(ctx, audit_model.UserPasswordResetRequest, doer, user, user, "Requested password reset for user %s.", user.Name)
}
func RecordUserVisibility(ctx context.Context, doer, user *user_model.User) {
if user.IsOrganization() {
record(ctx, audit_model.OrganizationVisibility, doer, user, user, "Changed visibility of organization %s to %s.", user.Name, user.Visibility.String())
} else {
record(ctx, audit_model.UserVisibility, doer, user, user, "Changed visibility of user %s to %s.", user.Name, user.Visibility.String())
}
}
func RecordUserEmailPrimaryChange(ctx context.Context, doer, user *user_model.User, email *user_model.EmailAddress) {
record(ctx, audit_model.UserEmailPrimaryChange, doer, user, email, "Changed primary email of user %s to %s.", user.Name, email.Email)
}
func RecordUserEmailAdd(ctx context.Context, doer, user *user_model.User, email *user_model.EmailAddress) {
record(ctx, audit_model.UserEmailAdd, doer, user, email, "Added email %s to user %s.", email.Email, user.Name)
}
func RecordUserEmailActivate(ctx context.Context, doer, user *user_model.User, email *user_model.EmailAddress) {
status := "active"
if !email.IsActivated {
status = "inactive"
}
record(ctx, audit_model.UserEmailActivate, doer, user, email, "Changed activation status of email %s of user %s to %s.", email.Email, user.Name, status)
}
func RecordUserEmailRemove(ctx context.Context, doer, user *user_model.User, email *user_model.EmailAddress) {
record(ctx, audit_model.UserEmailRemove, doer, user, email, "Removed email %s from user %s.", email.Email, user.Name)
}
func RecordUserTwoFactorEnable(ctx context.Context, doer, user *user_model.User) {
record(ctx, audit_model.UserTwoFactorEnable, doer, user, user, "Enabled two-factor authentication for user %s.", user.Name)
}
func RecordUserTwoFactorRegenerate(ctx context.Context, doer, user *user_model.User, tf *auth_model.TwoFactor) {
record(ctx, audit_model.UserTwoFactorRegenerate, doer, user, tf, "Regenerated two-factor authentication secret for user %s.", user.Name)
}
func RecordUserTwoFactorDisable(ctx context.Context, doer, user *user_model.User, tf *auth_model.TwoFactor) {
record(ctx, audit_model.UserTwoFactorDisable, doer, user, tf, "Disabled two-factor authentication for user %s.", user.Name)
}
func RecordUserWebAuthAdd(ctx context.Context, doer, user *user_model.User, authn *auth_model.WebAuthnCredential) {
record(ctx, audit_model.UserWebAuthAdd, doer, user, authn, "Added WebAuthn key %s for user %s.", authn.Name, user.Name)
}
func RecordUserWebAuthRemove(ctx context.Context, doer, user *user_model.User, authn *auth_model.WebAuthnCredential) {
record(ctx, audit_model.UserWebAuthRemove, doer, user, authn, "Removed WebAuthn key %s from user %s.", authn.Name, user.Name)
}
func RecordUserExternalLoginAdd(ctx context.Context, doer, user *user_model.User, externalLogin *user_model.ExternalLoginUser) {
record(ctx, audit_model.UserExternalLoginAdd, doer, user, "Added external login %s for user %s using provider %s.", externalLogin.ExternalID, user.Name, externalLogin.Provider)
}
func RecordUserExternalLoginRemove(ctx context.Context, doer, user *user_model.User, externalLogin *user_model.ExternalLoginUser) {
record(ctx, audit_model.UserExternalLoginRemove, doer, user, "Removed external login %s for user %s from provider.", externalLogin.ExternalID, user.Name, externalLogin.Provider)
}
func RecordUserOpenIDAdd(ctx context.Context, doer, user *user_model.User, oid *user_model.UserOpenID) {
record(ctx, audit_model.UserOpenIDAdd, doer, user, oid, "Associated OpenID %s to user %s.", oid.URI, user.Name)
}
func RecordUserOpenIDRemove(ctx context.Context, doer, user *user_model.User, oid *user_model.UserOpenID) {
record(ctx, audit_model.UserOpenIDRemove, doer, user, oid, "Removed OpenID %s from user %s.", oid.URI, user.Name)
}
func RecordUserAccessTokenAdd(ctx context.Context, doer, user *user_model.User, token *auth_model.AccessToken) {
record(ctx, audit_model.UserAccessTokenAdd, doer, user, token, "Added access token %s for user %s with scope %s.", token.Name, user.Name, token.Scope)
}
func RecordUserAccessTokenRemove(ctx context.Context, doer, user *user_model.User, token *auth_model.AccessToken) {
record(ctx, audit_model.UserAccessTokenRemove, doer, user, token, "Removed access token %s from user %s.", token.Name, user.Name)
}
func RecordOAuth2ApplicationAdd(ctx context.Context, doer, user *user_model.User, app *auth_model.OAuth2Application) {
if user == nil {
record(ctx, audit_model.SystemOAuth2ApplicationAdd, doer, &systemObject, app, "Created instance-wide OAuth2 application %s", app.Name)
} else if user.IsOrganization() {
record(ctx, audit_model.OrganizationOAuth2ApplicationAdd, doer, user, app, "Created OAuth2 application %s for organization %s", app.Name, user.Name)
} else {
record(ctx, audit_model.UserOAuth2ApplicationAdd, doer, user, app, "Created OAuth2 application %s for user %s", app.Name, user.Name)
}
}
func RecordOAuth2ApplicationUpdate(ctx context.Context, doer, user *user_model.User, app *auth_model.OAuth2Application) {
if user == nil {
record(ctx, audit_model.SystemOAuth2ApplicationUpdate, doer, &systemObject, app, "Updated instance-wide OAuth2 application %s", app.Name)
} else if user.IsOrganization() {
record(ctx, audit_model.OrganizationOAuth2ApplicationUpdate, doer, user, app, "Updated OAuth2 application %s of organization %s", app.Name, user.Name)
} else {
record(ctx, audit_model.UserOAuth2ApplicationUpdate, doer, user, app, "Updated OAuth2 application %s of user %s", app.Name, user.Name)
}
}
func RecordOAuth2ApplicationSecret(ctx context.Context, doer, user *user_model.User, app *auth_model.OAuth2Application) {
if user == nil {
record(ctx, audit_model.SystemOAuth2ApplicationSecret, doer, &systemObject, app, "Regenerated secret for instance-wide OAuth2 application %s", app.Name)
} else if user.IsOrganization() {
record(ctx, audit_model.OrganizationOAuth2ApplicationSecret, doer, user, app, "Regenerated secret for OAuth2 application %s of organization %s", app.Name, user.Name)
} else {
record(ctx, audit_model.UserOAuth2ApplicationSecret, doer, user, app, "Regenerated secret for OAuth2 application %s of user %s", app.Name, user.Name)
}
}
func RecordUserOAuth2ApplicationGrant(ctx context.Context, doer, owner *user_model.User, app *auth_model.OAuth2Application, grant *auth_model.OAuth2Grant) {
record(ctx, audit_model.UserOAuth2ApplicationGrant, doer, owner, grant, "Granted OAuth2 access to application %s of user %s.", app.Name, owner.Name)
}
func RecordUserOAuth2ApplicationRevoke(ctx context.Context, doer, owner *user_model.User, app *auth_model.OAuth2Application, grant *auth_model.OAuth2Grant) {
record(ctx, audit_model.UserOAuth2ApplicationRevoke, doer, owner, grant, "Revoked OAuth2 grant for application %s of user %s.", app.Name, owner.Name)
}
func RecordOAuth2ApplicationRemove(ctx context.Context, doer, user *user_model.User, app *auth_model.OAuth2Application) {
if user == nil {
record(ctx, audit_model.SystemOAuth2ApplicationRemove, doer, &systemObject, app, "Removed instance-wide OAuth2 application %s", app.Name)
} else if user.IsOrganization() {
record(ctx, audit_model.OrganizationOAuth2ApplicationRemove, doer, user, app, "Removed OAuth2 application %s of organization %s", app.Name, user.Name)
} else {
record(ctx, audit_model.UserOAuth2ApplicationRemove, doer, user, app, "Removed OAuth2 application %s of user %s", app.Name, user.Name)
}
}
func RecordUserKeySSHAdd(ctx context.Context, doer, user *user_model.User, key *asymkey_model.PublicKey) {
record(ctx, audit_model.UserKeySSHAdd, doer, user, key, "Added SSH key %s for user %s.", key.Fingerprint, user.Name)
}
func RecordUserKeySSHRemove(ctx context.Context, doer, user *user_model.User, key *asymkey_model.PublicKey) {
record(ctx, audit_model.UserKeySSHRemove, doer, user, key, "Removed SSH key %s of user %s.", key.Fingerprint, user.Name)
}
func RecordUserKeyPrincipalAdd(ctx context.Context, doer, user *user_model.User, key *asymkey_model.PublicKey) {
record(ctx, audit_model.UserKeyPrincipalAdd, doer, user, key, "Added principal key %s for user %s.", key.Name, user.Name)
}
func RecordUserKeyPrincipalRemove(ctx context.Context, doer, user *user_model.User, key *asymkey_model.PublicKey) {
record(ctx, audit_model.UserKeyPrincipalRemove, doer, user, key, "Removed principal key %s of user %s.", key.Name, user.Name)
}
func RecordUserKeyGPGAdd(ctx context.Context, doer, user *user_model.User, key *asymkey_model.GPGKey) {
record(ctx, audit_model.UserKeyGPGAdd, doer, user, key, "Added GPG key %s for user %s.", key.KeyID, user.Name)
}
func RecordUserKeyGPGRemove(ctx context.Context, doer, user *user_model.User, key *asymkey_model.GPGKey) {
record(ctx, audit_model.UserKeyGPGRemove, doer, user, key, "Removed GPG key %s of user %s.", key.KeyID, user.Name)
}
func RecordSecretAdd(ctx context.Context, doer, owner *user_model.User, repo *repository_model.Repository, secret *secret_model.Secret) {
if owner == nil {
record(ctx, audit_model.RepositorySecretAdd, doer, repo, secret, "Added secret %s for repository %s.", secret.Name, repo.FullName())
} else if owner.IsOrganization() {
record(ctx, audit_model.OrganizationSecretAdd, doer, owner, secret, "Added secret %s for organization %s.", secret.Name, owner.Name)
} else {
record(ctx, audit_model.UserSecretAdd, doer, owner, secret, "Added secret %s for user %s.", secret.Name, owner.Name)
}
}
func RecordSecretUpdate(ctx context.Context, doer, owner *user_model.User, repo *repository_model.Repository, secret *secret_model.Secret) {
if owner == nil {
record(ctx, audit_model.RepositorySecretUpdate, doer, repo, secret, "Updated secret %s of repository %s.", secret.Name, repo.FullName())
} else if owner.IsOrganization() {
record(ctx, audit_model.OrganizationSecretUpdate, doer, owner, secret, "Updated secret %s of organization %s.", secret.Name, owner.Name)
} else {
record(ctx, audit_model.UserSecretUpdate, doer, owner, secret, "Updated secret %s of user %s.", secret.Name, owner.Name)
}
}
func RecordSecretRemove(ctx context.Context, doer, owner *user_model.User, repo *repository_model.Repository, secret *secret_model.Secret) {
if owner == nil {
record(ctx, audit_model.RepositorySecretRemove, doer, repo, secret, "Removed secret %s of repository %s.", secret.Name, repo.FullName())
} else if owner.IsOrganization() {
record(ctx, audit_model.OrganizationSecretRemove, doer, owner, secret, "Removed secret %s of organization %s.", secret.Name, owner.Name)
} else {
record(ctx, audit_model.UserSecretRemove, doer, owner, secret, "Removed secret %s of user %s.", secret.Name, owner.Name)
}
}
func RecordWebhookAdd(ctx context.Context, doer, owner *user_model.User, repo *repository_model.Repository, hook *webhook_model.Webhook) {
if owner == nil && repo == nil {
record(ctx, audit_model.SystemWebhookAdd, doer, &systemObject, hook, "Added instance-wide webhook %s.", hook.URL)
} else if repo != nil {
record(ctx, audit_model.RepositoryWebhookAdd, doer, repo, hook, "Added webhook %s for repository %s.", hook.URL, repo.FullName())
} else if owner.IsOrganization() {
record(ctx, audit_model.OrganizationWebhookAdd, doer, owner, hook, "Added webhook %s for organization %s.", hook.URL, owner.Name)
} else {
record(ctx, audit_model.UserWebhookAdd, doer, owner, hook, "Added webhook %s for user %s.", hook.URL, owner.Name)
}
}
func RecordWebhookUpdate(ctx context.Context, doer, owner *user_model.User, repo *repository_model.Repository, hook *webhook_model.Webhook) {
if owner == nil && repo == nil {
record(ctx, audit_model.SystemWebhookUpdate, doer, &systemObject, hook, "Updated instance-wide webhook %s.", hook.URL)
} else if repo != nil {
record(ctx, audit_model.RepositoryWebhookUpdate, doer, repo, hook, "Updated webhook %s of repository %s.", hook.URL, repo.FullName())
} else if owner.IsOrganization() {
record(ctx, audit_model.OrganizationWebhookUpdate, doer, owner, hook, "Updated webhook %s of organization %s.", hook.URL, owner.Name)
} else {
record(ctx, audit_model.UserWebhookUpdate, doer, owner, hook, "Updated webhook %s of user %s.", hook.URL, owner.Name)
}
}
func RecordWebhookRemove(ctx context.Context, doer, owner *user_model.User, repo *repository_model.Repository, hook *webhook_model.Webhook) {
if owner == nil && repo == nil {
record(ctx, audit_model.SystemWebhookRemove, doer, &systemObject, hook, "Removed instance-wide webhook %s.", hook.URL)
} else if repo != nil {
record(ctx, audit_model.RepositoryWebhookRemove, doer, repo, hook, "Removed webhook %s of repository %s.", hook.URL, repo.FullName())
} else if owner.IsOrganization() {
record(ctx, audit_model.OrganizationWebhookRemove, doer, owner, hook, "Removed webhook %s of organization %s.", hook.URL, owner.Name)
} else {
record(ctx, audit_model.UserWebhookRemove, doer, owner, hook, "Removed webhook %s of user %s.", hook.URL, owner.Name)
}
}
func RecordOrganizationTeamAdd(ctx context.Context, doer *user_model.User, org *organization_model.Organization, team *organization_model.Team) {
record(ctx, audit_model.OrganizationTeamAdd, doer, org, team, "Added team %s to organization %s.", team.Name, org.Name)
}
func RecordOrganizationTeamUpdate(ctx context.Context, doer *user_model.User, org *organization_model.Organization, team *organization_model.Team) {
record(ctx, audit_model.OrganizationTeamUpdate, doer, org, team, "Updated settings of team %s/%s.", org.Name, team.Name)
}
func RecordOrganizationTeamRemove(ctx context.Context, doer *user_model.User, org *organization_model.Organization, team *organization_model.Team) {
record(ctx, audit_model.OrganizationTeamRemove, doer, org, team, "Removed team %s from organization %s.", team.Name, org.Name)
}
func RecordOrganizationTeamPermission(ctx context.Context, doer *user_model.User, org *organization_model.Organization, team *organization_model.Team) {
record(ctx, audit_model.OrganizationTeamPermission, doer, org, team, "Changed permission of team %s/%s to %s.", org.Name, team.Name, team.AccessMode.ToString())
}
func RecordOrganizationTeamMemberAdd(ctx context.Context, doer *user_model.User, org *organization_model.Organization, team *organization_model.Team, member *user_model.User) {
record(ctx, audit_model.OrganizationTeamMemberAdd, doer, org, team, "Added user %s to team %s/%s.", member.Name, org.Name, team.Name)
}
func RecordOrganizationTeamMemberRemove(ctx context.Context, doer *user_model.User, org *organization_model.Organization, team *organization_model.Team, member *user_model.User) {
record(ctx, audit_model.OrganizationTeamMemberRemove, doer, org, team, "Removed user %s from team %s/%s.", member.Name, org.Name, team.Name)
}
func RecordRepositoryCreate(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryCreate, doer, repo, repo, "Created repository %s.", repo.FullName())
}
func RecordRepositoryCreateFork(ctx context.Context, doer *user_model.User, repo, baseRepo *repository_model.Repository) {
record(ctx, audit_model.RepositoryCreateFork, doer, repo, repo, "Created fork %s of repository %s.", repo.FullName(), baseRepo.FullName())
}
func RecordRepositoryArchive(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryArchive, doer, repo, repo, "Archived repository %s.", repo.FullName())
}
func RecordRepositoryUnarchive(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryUnarchive, doer, repo, repo, "Unarchived repository %s.", repo.FullName())
}
func RecordRepositoryDelete(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryDelete, doer, repo, repo, "Deleted repository %s.", repo.FullName())
}
func RecordRepositoryName(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, previousName string) {
record(ctx, audit_model.RepositoryName, doer, repo, repo, "Changed repository name from %s to %s.", previousName, repo.FullName())
}
func RecordRepositoryVisibility(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
status := "public"
if repo.IsPrivate {
status = "private"
}
record(ctx, audit_model.RepositoryVisibility, doer, repo, repo, "Changed visibility of repository %s to %s.", repo.FullName(), status)
}
func RecordRepositoryConvertFork(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryConvertFork, doer, repo, repo, "Converted repository %s from fork to regular repository.", repo.FullName())
}
func RecordRepositoryConvertMirror(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryConvertMirror, doer, repo, repo, "Converted repository %s from pull mirror to regular repository.", repo.FullName())
}
func RecordRepositoryMirrorPushAdd(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, mirror *repository_model.PushMirror) {
record(ctx, audit_model.RepositoryMirrorPushAdd, doer, repo, mirror, "Added push mirror to %s for repository %s.", mirror.RemoteAddress, repo.FullName())
}
func RecordRepositoryMirrorPushRemove(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, mirror *repository_model.PushMirror) {
record(ctx, audit_model.RepositoryMirrorPushRemove, doer, repo, mirror, "Removed push mirror to %s for repository %s.", mirror.RemoteAddress, repo.FullName())
}
func RecordRepositorySigningVerification(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositorySigningVerification, doer, repo, repo, "Changed signing verification of repository %s to %s.", repo.FullName(), repo.TrustModel.String())
}
func RecordRepositoryTransferStart(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, newOwner *user_model.User) {
record(ctx, audit_model.RepositoryTransferStart, doer, repo, repo, "Started repository transfer of %s to %s.", repo.FullName(), newOwner.Name)
}
func RecordRepositoryTransferFinish(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, oldOwner *user_model.User) {
record(ctx, audit_model.RepositoryTransferFinish, doer, repo, repo, "Transferred repository %s from %s to %s.", repo.FullName(), oldOwner.Name, repo.OwnerName)
}
func RecordRepositoryTransferCancel(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryTransferCancel, doer, repo, repo, "Canceled transfer of repository %s.", repo.FullName())
}
func RecordRepositoryWikiDelete(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryWikiDelete, doer, repo, repo, "Deleted wiki of repository %s.", repo.FullName())
}
func RecordRepositoryCollaboratorAdd(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, collaborator *user_model.User) {
record(ctx, audit_model.RepositoryCollaboratorAdd, doer, repo, collaborator, "Added user %s as collaborator for repository %s.", collaborator.Name, repo.FullName())
}
func RecordRepositoryCollaboratorAccess(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, collaborator *user_model.User, accessMode perm_model.AccessMode) {
record(ctx, audit_model.RepositoryCollaboratorAccess, doer, repo, collaborator, "Changed access mode of collaborator %s of repository %s to %s.", collaborator.Name, repo.FullName(), accessMode.ToString())
}
func RecordRepositoryCollaboratorRemove(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, collaborator *user_model.User) {
record(ctx, audit_model.RepositoryCollaboratorRemove, doer, repo, collaborator, "Removed collaborator %s from repository %s.", collaborator.Name, repo.FullName())
}
func RecordRepositoryCollaboratorTeamAdd(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, team *organization_model.Team) {
record(ctx, audit_model.RepositoryCollaboratorTeamAdd, doer, repo, team, "Added team %s as collaborator for repository %s.", team.Name, repo.FullName())
}
func RecordRepositoryCollaboratorTeamRemove(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, team *organization_model.Team) {
record(ctx, audit_model.RepositoryCollaboratorTeamRemove, doer, repo, team, "Removed team %s as collaborator from repository %s.", team.Name, repo.FullName())
}
func RecordRepositoryBranchDefault(ctx context.Context, doer *user_model.User, repo *repository_model.Repository) {
record(ctx, audit_model.RepositoryBranchDefault, doer, repo, repo, "Changed default branch of repository %s to %s.", repo.FullName(), repo.DefaultBranch)
}
func RecordRepositoryBranchProtectionAdd(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, protectBranch *git_model.ProtectedBranch) {
record(ctx, audit_model.RepositoryBranchProtectionAdd, doer, repo, protectBranch, "Added branch protection %s for repository %s.", protectBranch.RuleName, repo.FullName())
}
func RecordRepositoryBranchProtectionUpdate(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, protectBranch *git_model.ProtectedBranch) {
record(ctx, audit_model.RepositoryBranchProtectionUpdate, doer, repo, protectBranch, "Updated branch protection %s for repository %s.", protectBranch.RuleName, repo.FullName())
}
func RecordRepositoryBranchProtectionRemove(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, protectBranch *git_model.ProtectedBranch) {
record(ctx, audit_model.RepositoryBranchProtectionRemove, doer, repo, protectBranch, "Removed branch protection %s from repository %s.", protectBranch.RuleName, repo.FullName())
}
func RecordRepositoryTagProtectionAdd(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, protectedTag *git_model.ProtectedTag) {
record(ctx, audit_model.RepositoryTagProtectionAdd, doer, repo, protectedTag, "Added tag protection %s for repository %s.", protectedTag.NamePattern, repo.FullName())
}
func RecordRepositoryTagProtectionUpdate(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, protectedTag *git_model.ProtectedTag) {
record(ctx, audit_model.RepositoryTagProtectionUpdate, doer, repo, protectedTag, "Updated tag protection %s for repository %s.", protectedTag.NamePattern, repo.FullName())
}
func RecordRepositoryTagProtectionRemove(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, protectedTag *git_model.ProtectedTag) {
record(ctx, audit_model.RepositoryTagProtectionRemove, doer, repo, protectedTag, "Removed tag protection %s for repository %s.", protectedTag.NamePattern, repo.FullName())
}
func RecordRepositoryDeployKeyAdd(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, deployKey *asymkey_model.DeployKey) {
record(ctx, audit_model.RepositoryDeployKeyAdd, doer, repo, deployKey, "Added deploy key %s for repository %s.", deployKey.Name, repo.FullName())
}
func RecordRepositoryDeployKeyRemove(ctx context.Context, doer *user_model.User, repo *repository_model.Repository, deployKey *asymkey_model.DeployKey) {
record(ctx, audit_model.RepositoryDeployKeyRemove, doer, repo, deployKey, "Removed deploy key %s from repository %s.", deployKey.Name, repo.FullName())
}
func RecordSystemStartup(ctx context.Context, doer *user_model.User, version string) {
// Do not change this message anymore. We guarantee the stability of this message for users wanting to parse the log themselves to be able to trace back events across gitea versions.
record(ctx, audit_model.SystemStartup, doer, &systemObject, &systemObject, "System started [Gitea %s]", version)
}
func RecordSystemShutdown(ctx context.Context, doer *user_model.User) {
record(ctx, audit_model.SystemShutdown, doer, &systemObject, &systemObject, "System shutdown")
}
func RecordSystemAuthenticationSourceAdd(ctx context.Context, doer *user_model.User, authSource *auth_model.Source) {
record(ctx, audit_model.SystemAuthenticationSourceAdd, doer, &systemObject, authSource, "Created authentication source %s of type %s.", authSource.Name, authSource.Type.String())
}
func RecordSystemAuthenticationSourceUpdate(ctx context.Context, doer *user_model.User, authSource *auth_model.Source) {
record(ctx, audit_model.SystemAuthenticationSourceUpdate, doer, &systemObject, authSource, "Updated authentication source %s.", authSource.Name)
}
func RecordSystemAuthenticationSourceRemove(ctx context.Context, doer *user_model.User, authSource *auth_model.Source) {
record(ctx, audit_model.SystemAuthenticationSourceRemove, doer, &systemObject, authSource, "Removed authentication source %s.", authSource.Name)
}

View File

@ -95,7 +95,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
opts := &user_service.UpdateOptions{
Language: optional.Some(lc.Language()),
}
if err := user_service.UpdateUser(req.Context(), user, opts); err != nil {
if err := user_service.UpdateUser(req.Context(), user, user, opts); err != nil {
log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", user.ID, user.Language))
return
}

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/audit"
)
// Ensure the struct implements the interface.
@ -155,6 +156,8 @@ func validateTOTP(req *http.Request, u *user_model.User) error {
if ok, err := twofa.ValidateTOTP(req.Header.Get("X-Gitea-OTP")); err != nil {
return err
} else if !ok {
audit.RecordUserAuthenticationFailTwoFactor(req.Context(), u)
return util.NewInvalidArgumentErrorf("invalid provided OTP")
}
return nil

Some files were not shown because too many files have changed in this diff Show More