diff --git a/.drone.yml b/.drone.yml index dc8423875a..7150629028 100644 --- a/.drone.yml +++ b/.drone.yml @@ -985,7 +985,10 @@ depends_on: trigger: ref: - - "refs/tags/**" + include: + - "refs/tags/**" + exclude: + - "refs/tags/**-rc*" event: exclude: - cron @@ -1033,6 +1036,68 @@ steps: event: exclude: - pull_request +--- + +kind: pipeline +type: docker +name: docker-linux-amd64-release-candidate-version + +platform: + os: linux + arch: amd64 + +depends_on: + - testing-amd64 + - testing-arm64 + +trigger: + ref: + - "refs/tags/**-rc*" + event: + exclude: + - cron + +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + + - name: publish + image: techknowlogick/drone-docker:latest + pull: always + settings: + tags: ${DRONE_TAG##v}-linux-amd64 + repo: gitea/gitea + build_args: + - GOPROXY=https://goproxy.io + password: + from_secret: docker_password + username: + from_secret: docker_username + when: + event: + exclude: + - pull_request + + - name: publish-rootless + image: techknowlogick/drone-docker:latest + settings: + dockerfile: Dockerfile.rootless + tags: ${DRONE_TAG##v}-linux-amd64-rootless + repo: gitea/gitea + build_args: + - GOPROXY=https://goproxy.io + password: + from_secret: docker_password + username: + from_secret: docker_username + when: + event: + exclude: + - pull_request --- kind: pipeline @@ -1209,7 +1274,10 @@ depends_on: trigger: ref: - - "refs/tags/**" + include: + - "refs/tags/**" + exclude: + - "refs/tags/**-rc*" event: exclude: - cron @@ -1258,6 +1326,68 @@ steps: exclude: - pull_request +--- +kind: pipeline +type: docker +name: docker-linux-arm64-release-candidate-version + +platform: + os: linux + arch: arm64 + +depends_on: + - testing-amd64 + - testing-arm64 + +trigger: + ref: + - "refs/tags/**-rc*" + event: + exclude: + - cron + +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + + - name: publish + image: techknowlogick/drone-docker:latest + pull: always + settings: + tags: ${DRONE_TAG##v}-linux-arm64 + repo: gitea/gitea + build_args: + - GOPROXY=https://goproxy.io + password: + from_secret: docker_password + username: + from_secret: docker_username + when: + event: + exclude: + - pull_request + + - name: publish-rootless + image: techknowlogick/drone-docker:latest + settings: + dockerfile: Dockerfile.rootless + tags: ${DRONE_TAG##v}-linux-arm64-rootless + repo: gitea/gitea + build_args: + - GOPROXY=https://goproxy.io + password: + from_secret: docker_password + username: + from_secret: docker_username + when: + event: + exclude: + - pull_request + --- kind: pipeline type: docker @@ -1427,7 +1557,9 @@ trigger: depends_on: - docker-linux-amd64-release-version + - docker-linux-amd64-release-candidate-version - docker-linux-arm64-release-version + - docker-linux-arm64-release-candidate-version --- kind: pipeline @@ -1509,6 +1641,8 @@ depends_on: - docker-linux-arm64-release - docker-linux-amd64-release-version - docker-linux-arm64-release-version + - docker-linux-amd64-release-candidate-version + - docker-linux-arm64-release-candidate-version - docker-linux-amd64-release-branch - docker-linux-arm64-release-branch - docker-manifest diff --git a/.eslintrc.yaml b/.eslintrc.yaml index fdd86a4647..a73df2ee34 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -149,7 +149,7 @@ rules: jquery/no-global-eval: [2] jquery/no-grep: [2] jquery/no-has: [2] - jquery/no-hide: [0] + jquery/no-hide: [2] jquery/no-html: [0] jquery/no-in-array: [2] jquery/no-is-array: [2] @@ -166,13 +166,13 @@ rules: jquery/no-proxy: [2] jquery/no-ready: [0] jquery/no-serialize: [2] - jquery/no-show: [0] + jquery/no-show: [2] jquery/no-size: [2] jquery/no-sizzle: [0] jquery/no-slide: [0] jquery/no-submit: [0] jquery/no-text: [0] - jquery/no-toggle: [0] + jquery/no-toggle: [2] jquery/no-trigger: [0] jquery/no-trim: [2] jquery/no-val: [0] diff --git a/.golangci.yml b/.golangci.yml index aa04e0a8ef..263149f773 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,7 +28,7 @@ linters: fast: false run: - go: 1.20 + go: "1.20" timeout: 10m skip-dirs: - node_modules diff --git a/MAINTAINERS b/MAINTAINERS index 74196d4bd8..a37d336de3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6,7 +6,6 @@ Kim Carlbäcker (@bkcsoft) LefsFlare (@LefsFlarey) Lunny Xiao (@lunny) Matthias Loibl (@metalmatze) -Morgan Bazalgette (@thehowl) Rachid Zarouali (@xinity) Rémy Boulanouar (@DblK) Sandro Santilli (@strk) diff --git a/cmd/admin.go b/cmd/admin.go index 318c212d08..b913b817bd 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -5,7 +5,6 @@ package cmd import ( - "context" "errors" "fmt" "os" @@ -16,20 +15,15 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "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/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" - pwd "code.gitea.io/gitea/modules/password" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/smtp" repo_service "code.gitea.io/gitea/services/repository" - user_service "code.gitea.io/gitea/services/user" "github.com/urfave/cli" ) @@ -48,147 +42,6 @@ var ( }, } - subcmdUser = cli.Command{ - Name: "user", - Usage: "Modify users", - Subcommands: []cli.Command{ - microcmdUserCreate, - microcmdUserList, - microcmdUserChangePassword, - microcmdUserDelete, - microcmdUserGenerateAccessToken, - }, - } - - microcmdUserList = cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", - }, - }, - } - - microcmdUserCreate = cli.Command{ - Name: "create", - Usage: "Create a new user in database", - Action: runCreateUser, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "name", - Usage: "Username. DEPRECATED: use username instead", - }, - cli.StringFlag{ - Name: "username", - Usage: "Username", - }, - cli.StringFlag{ - Name: "password", - Usage: "User password", - }, - cli.StringFlag{ - Name: "email", - Usage: "User email address", - }, - cli.BoolFlag{ - Name: "admin", - Usage: "User is an admin", - }, - cli.BoolFlag{ - Name: "random-password", - Usage: "Generate a random password for the user", - }, - cli.BoolFlag{ - Name: "must-change-password", - Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)", - }, - cli.IntFlag{ - Name: "random-password-length", - Usage: "Length of the random password to be generated", - Value: 12, - }, - cli.BoolFlag{ - Name: "access-token", - Usage: "Generate access token for the user", - }, - cli.BoolFlag{ - Name: "restricted", - Usage: "Make a restricted user account", - }, - }, - } - - microcmdUserChangePassword = cli.Command{ - Name: "change-password", - Usage: "Change a user's password", - Action: runChangePassword, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "username,u", - Value: "", - Usage: "The user to change password for", - }, - cli.StringFlag{ - Name: "password,p", - Value: "", - Usage: "New password to set for user", - }, - }, - } - - microcmdUserDelete = cli.Command{ - Name: "delete", - Usage: "Delete specific user by id, name or email", - Flags: []cli.Flag{ - cli.Int64Flag{ - Name: "id", - Usage: "ID of user of the user to delete", - }, - cli.StringFlag{ - Name: "username,u", - Usage: "Username of the user to delete", - }, - cli.StringFlag{ - Name: "email,e", - Usage: "Email of the user to delete", - }, - cli.BoolFlag{ - Name: "purge", - Usage: "Purge user, all their repositories, organizations and comments", - }, - }, - Action: runDeleteUser, - } - - microcmdUserGenerateAccessToken = cli.Command{ - Name: "generate-access-token", - Usage: "Generate a access token for a specific user", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "username,u", - Usage: "Username", - }, - cli.StringFlag{ - Name: "token-name,t", - Usage: "Token name", - Value: "gitea-admin", - }, - cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - cli.StringFlag{ - Name: "scopes", - Value: "", - Usage: "Comma separated list of scopes to apply to access token", - }, - }, - Action: runGenerateAccessToken, - } - subcmdRepoSyncReleases = cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", @@ -486,265 +339,6 @@ var ( } ) -func runChangePassword(c *cli.Context) error { - if err := argsSet(c, "username", "password"); err != nil { - return err - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - if len(c.String("password")) < setting.MinPasswordLength { - return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) - } - - if !pwd.IsComplexEnough(c.String("password")) { - return errors.New("Password does not meet complexity requirements") - } - pwned, err := pwd.IsPwned(context.Background(), c.String("password")) - if err != nil { - return err - } - if pwned { - return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") - } - uname := c.String("username") - user, err := user_model.GetUserByName(ctx, uname) - if err != nil { - return err - } - if err = user.SetPassword(c.String("password")); err != nil { - return err - } - - if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil { - return err - } - - fmt.Printf("%s's password has been successfully updated!\n", user.Name) - return nil -} - -func runCreateUser(c *cli.Context) error { - if err := argsSet(c, "email"); err != nil { - return err - } - - if c.IsSet("name") && c.IsSet("username") { - return errors.New("Cannot set both --name and --username flags") - } - if !c.IsSet("name") && !c.IsSet("username") { - return errors.New("One of --name or --username flags must be set") - } - - if c.IsSet("password") && c.IsSet("random-password") { - return errors.New("cannot set both -random-password and -password flags") - } - - var username string - if c.IsSet("username") { - username = c.String("username") - } else { - username = c.String("name") - fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - var password string - if c.IsSet("password") { - password = c.String("password") - } else if c.IsSet("random-password") { - var err error - password, err = pwd.Generate(c.Int("random-password-length")) - if err != nil { - return err - } - fmt.Printf("generated random password is '%s'\n", password) - } else { - return errors.New("must set either password or random-password flag") - } - - // always default to true - changePassword := true - - // If this is the first user being created. - // Take it as the admin and don't force a password update. - if n := user_model.CountUsers(nil); n == 0 { - changePassword = false - } - - if c.IsSet("must-change-password") { - changePassword = c.Bool("must-change-password") - } - - restricted := util.OptionalBoolNone - - if c.IsSet("restricted") { - restricted = util.OptionalBoolOf(c.Bool("restricted")) - } - - // default user visibility in app.ini - visibility := setting.Service.DefaultUserVisibilityMode - - u := &user_model.User{ - Name: username, - Email: c.String("email"), - Passwd: password, - IsAdmin: c.Bool("admin"), - MustChangePassword: changePassword, - Visibility: visibility, - } - - overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, - IsRestricted: restricted, - } - - if err := user_model.CreateUser(u, overwriteDefault); err != nil { - return fmt.Errorf("CreateUser: %w", err) - } - - if c.Bool("access-token") { - t := &auth_model.AccessToken{ - Name: "gitea-admin", - UID: u.ID, - } - - if err := auth_model.NewAccessToken(t); err != nil { - return err - } - - fmt.Printf("Access token was successfully created... %s\n", t.Token) - } - - fmt.Printf("New user '%s' has been successfully created!\n", username) - return nil -} - -func runListUsers(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - users, err := user_model.GetAllUsers() - if err != nil { - return err - } - - w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) - - if c.IsSet("admin") { - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") - for _, u := range users { - if u.IsAdmin { - fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) - } - } - } else { - twofa := user_model.UserList(users).GetTwoFaStatus() - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") - for _, u := range users { - fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) - } - - } - - w.Flush() - return nil -} - -func runDeleteUser(c *cli.Context) error { - if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { - return fmt.Errorf("You must provide the id, username or email of a user to delete") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - if err := storage.Init(); err != nil { - return err - } - - var err error - var user *user_model.User - if c.IsSet("email") { - user, err = user_model.GetUserByEmail(c.String("email")) - } else if c.IsSet("username") { - user, err = user_model.GetUserByName(ctx, c.String("username")) - } else { - user, err = user_model.GetUserByID(ctx, c.Int64("id")) - } - if err != nil { - return err - } - if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { - return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) - } - - if c.IsSet("id") && user.ID != c.Int64("id") { - 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")) -} - -func runGenerateAccessToken(c *cli.Context) error { - if !c.IsSet("username") { - return fmt.Errorf("You must provide the username to generate a token for them") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - user, err := user_model.GetUserByName(ctx, c.String("username")) - if err != nil { - return err - } - - accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize() - if err != nil { - return err - } - - t := &auth_model.AccessToken{ - Name: c.String("token-name"), - UID: user.ID, - Scope: accessTokenScope, - } - - if err := auth_model.NewAccessToken(t); err != nil { - return err - } - - if c.Bool("raw") { - fmt.Printf("%s\n", t.Token) - } else { - fmt.Printf("Access token was successfully created: %s\n", t.Token) - } - - return nil -} - func runRepoSyncReleases(_ *cli.Context) error { ctx, cancel := installSignals() defer cancel() diff --git a/cmd/admin_user.go b/cmd/admin_user.go new file mode 100644 index 0000000000..a442b8fe9c --- /dev/null +++ b/cmd/admin_user.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "github.com/urfave/cli" +) + +var subcmdUser = cli.Command{ + Name: "user", + Usage: "Modify users", + Subcommands: []cli.Command{ + microcmdUserCreate, + microcmdUserList, + microcmdUserChangePassword, + microcmdUserDelete, + microcmdUserGenerateAccessToken, + microcmdUserMustChangePassword, + }, +} diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go new file mode 100644 index 0000000000..7866bde912 --- /dev/null +++ b/cmd/admin_user_change_password.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + + user_model "code.gitea.io/gitea/models/user" + pwd "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli" +) + +var microcmdUserChangePassword = cli.Command{ + Name: "change-password", + Usage: "Change a user's password", + Action: runChangePassword, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "username,u", + Value: "", + Usage: "The user to change password for", + }, + cli.StringFlag{ + Name: "password,p", + Value: "", + Usage: "New password to set for user", + }, + }, +} + +func runChangePassword(c *cli.Context) error { + if err := argsSet(c, "username", "password"); err != nil { + return err + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + if len(c.String("password")) < setting.MinPasswordLength { + return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) + } + + if !pwd.IsComplexEnough(c.String("password")) { + return errors.New("Password does not meet complexity requirements") + } + pwned, err := pwd.IsPwned(context.Background(), c.String("password")) + if err != nil { + return err + } + if pwned { + return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") + } + uname := c.String("username") + user, err := user_model.GetUserByName(ctx, uname) + if err != nil { + return err + } + if err = user.SetPassword(c.String("password")); err != nil { + return err + } + + if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil { + return err + } + + fmt.Printf("%s's password has been successfully updated!\n", user.Name) + return nil +} diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go new file mode 100644 index 0000000000..09eaad54be --- /dev/null +++ b/cmd/admin_user_create.go @@ -0,0 +1,169 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "errors" + "fmt" + "os" + + auth_model "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + pwd "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "github.com/urfave/cli" +) + +var microcmdUserCreate = cli.Command{ + Name: "create", + Usage: "Create a new user in database", + Action: runCreateUser, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Usage: "Username. DEPRECATED: use username instead", + }, + cli.StringFlag{ + Name: "username", + Usage: "Username", + }, + cli.StringFlag{ + Name: "password", + Usage: "User password", + }, + cli.StringFlag{ + Name: "email", + Usage: "User email address", + }, + cli.BoolFlag{ + Name: "admin", + Usage: "User is an admin", + }, + cli.BoolFlag{ + Name: "random-password", + Usage: "Generate a random password for the user", + }, + cli.BoolFlag{ + Name: "must-change-password", + Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)", + }, + cli.IntFlag{ + Name: "random-password-length", + Usage: "Length of the random password to be generated", + Value: 12, + }, + cli.BoolFlag{ + Name: "access-token", + Usage: "Generate access token for the user", + }, + cli.BoolFlag{ + Name: "restricted", + Usage: "Make a restricted user account", + }, + }, +} + +func runCreateUser(c *cli.Context) error { + if err := argsSet(c, "email"); err != nil { + return err + } + + if c.IsSet("name") && c.IsSet("username") { + return errors.New("Cannot set both --name and --username flags") + } + if !c.IsSet("name") && !c.IsSet("username") { + return errors.New("One of --name or --username flags must be set") + } + + if c.IsSet("password") && c.IsSet("random-password") { + return errors.New("cannot set both -random-password and -password flags") + } + + var username string + if c.IsSet("username") { + username = c.String("username") + } else { + username = c.String("name") + fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + var password string + if c.IsSet("password") { + password = c.String("password") + } else if c.IsSet("random-password") { + var err error + password, err = pwd.Generate(c.Int("random-password-length")) + if err != nil { + return err + } + fmt.Printf("generated random password is '%s'\n", password) + } else { + return errors.New("must set either password or random-password flag") + } + + // always default to true + changePassword := true + + // If this is the first user being created. + // Take it as the admin and don't force a password update. + if n := user_model.CountUsers(nil); n == 0 { + changePassword = false + } + + if c.IsSet("must-change-password") { + changePassword = c.Bool("must-change-password") + } + + restricted := util.OptionalBoolNone + + if c.IsSet("restricted") { + restricted = util.OptionalBoolOf(c.Bool("restricted")) + } + + // default user visibility in app.ini + visibility := setting.Service.DefaultUserVisibilityMode + + u := &user_model.User{ + Name: username, + Email: c.String("email"), + Passwd: password, + IsAdmin: c.Bool("admin"), + MustChangePassword: changePassword, + Visibility: visibility, + } + + overwriteDefault := &user_model.CreateUserOverwriteOptions{ + IsActive: util.OptionalBoolTrue, + IsRestricted: restricted, + } + + if err := user_model.CreateUser(u, overwriteDefault); err != nil { + return fmt.Errorf("CreateUser: %w", err) + } + + if c.Bool("access-token") { + t := &auth_model.AccessToken{ + Name: "gitea-admin", + UID: u.ID, + } + + if err := auth_model.NewAccessToken(t); err != nil { + return err + } + + fmt.Printf("Access token was successfully created... %s\n", t.Token) + } + + fmt.Printf("New user '%s' has been successfully created!\n", username) + return nil +} diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go new file mode 100644 index 0000000000..30d6d11576 --- /dev/null +++ b/cmd/admin_user_delete.go @@ -0,0 +1,78 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/storage" + user_service "code.gitea.io/gitea/services/user" + + "github.com/urfave/cli" +) + +var microcmdUserDelete = cli.Command{ + Name: "delete", + Usage: "Delete specific user by id, name or email", + Flags: []cli.Flag{ + cli.Int64Flag{ + Name: "id", + Usage: "ID of user of the user to delete", + }, + cli.StringFlag{ + Name: "username,u", + Usage: "Username of the user to delete", + }, + cli.StringFlag{ + Name: "email,e", + Usage: "Email of the user to delete", + }, + cli.BoolFlag{ + Name: "purge", + Usage: "Purge user, all their repositories, organizations and comments", + }, + }, + Action: runDeleteUser, +} + +func runDeleteUser(c *cli.Context) error { + if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { + return fmt.Errorf("You must provide the id, username or email of a user to delete") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + if err := storage.Init(); err != nil { + return err + } + + var err error + var user *user_model.User + if c.IsSet("email") { + user, err = user_model.GetUserByEmail(ctx, c.String("email")) + } else if c.IsSet("username") { + user, err = user_model.GetUserByName(ctx, c.String("username")) + } else { + user, err = user_model.GetUserByID(ctx, c.Int64("id")) + } + if err != nil { + return err + } + if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { + return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) + } + + if c.IsSet("id") && user.ID != c.Int64("id") { + 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")) +} diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go new file mode 100644 index 0000000000..822bc5c2bc --- /dev/null +++ b/cmd/admin_user_generate_access_token.go @@ -0,0 +1,80 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + + auth_model "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserGenerateAccessToken = cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "username,u", + Usage: "Username", + }, + cli.StringFlag{ + Name: "token-name,t", + Usage: "Token name", + Value: "gitea-admin", + }, + cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + cli.StringFlag{ + Name: "scopes", + Value: "", + Usage: "Comma separated list of scopes to apply to access token", + }, + }, + Action: runGenerateAccessToken, +} + +func runGenerateAccessToken(c *cli.Context) error { + if !c.IsSet("username") { + return fmt.Errorf("You must provide a username to generate a token for") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + user, err := user_model.GetUserByName(ctx, c.String("username")) + if err != nil { + return err + } + + accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize() + if err != nil { + return err + } + + t := &auth_model.AccessToken{ + Name: c.String("token-name"), + UID: user.ID, + Scope: accessTokenScope, + } + + if err := auth_model.NewAccessToken(t); err != nil { + return err + } + + if c.Bool("raw") { + fmt.Printf("%s\n", t.Token) + } else { + fmt.Printf("Access token was successfully created: %s\n", t.Token) + } + + return nil +} diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go new file mode 100644 index 0000000000..85490331ed --- /dev/null +++ b/cmd/admin_user_list.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserList = cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, + }, +} + +func runListUsers(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + users, err := user_model.GetAllUsers() + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) + + if c.IsSet("admin") { + fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") + for _, u := range users { + if u.IsAdmin { + fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) + } + } + } else { + twofa := user_model.UserList(users).GetTwoFaStatus() + fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") + for _, u := range users { + fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) + } + } + + w.Flush() + return nil +} diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go new file mode 100644 index 0000000000..eb13fbcae5 --- /dev/null +++ b/cmd/admin_user_must_change_password.go @@ -0,0 +1,58 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "errors" + "fmt" + + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserMustChangePassword = cli.Command{ + Name: "must-change-password", + Usage: "Set the must change password flag for the provided users or all users", + Action: runMustChangePassword, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all,A", + Usage: "All users must change password, except those explicitly excluded with --exclude", + }, + cli.StringSliceFlag{ + Name: "exclude,e", + Usage: "Do not change the must-change-password flag for these users", + }, + cli.BoolFlag{ + Name: "unset", + Usage: "Instead of setting the must-change-password flag, unset it", + }, + }, +} + +func runMustChangePassword(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + if c.NArg() == 0 && !c.IsSet("all") { + return errors.New("either usernames or --all must be provided") + } + + mustChangePassword := !c.Bool("unset") + all := c.Bool("all") + exclude := c.StringSlice("exclude") + + if err := initDB(ctx); err != nil { + return err + } + + n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args(), exclude) + if err != nil { + return err + } + + fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword) + return nil +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 493519e135..18d5db3987 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -57,9 +57,10 @@ func confirm() (bool, error) { } func initDB(ctx context.Context) error { - setting.LoadFromExisting() - setting.InitDBConfig() - setting.NewXORMLogService(false) + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() + setting.LoadDBSetting() + setting.InitSQLLog(false) if setting.Database.Type == "" { log.Fatal(`Database settings are missing from the configuration file: %q. diff --git a/cmd/convert.go b/cmd/convert.go index b9ed9f1627..30e7d01e11 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -32,7 +32,7 @@ func runConvert(ctx *cli.Context) error { log.Info("AppPath: %s", setting.AppPath) log.Info("AppWorkPath: %s", setting.AppWorkPath) log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.LogRootPath) + log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) if !setting.Database.UseMySQL { diff --git a/cmd/doctor.go b/cmd/doctor.go index ceb6e3fbab..e7baad60c1 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -87,14 +87,16 @@ func runRecreateTable(ctx *cli.Context) error { golog.SetPrefix("") golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT))) - setting.LoadFromExisting() - setting.InitDBConfig() + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() + setting.LoadDBSetting() - setting.EnableXORMLog = ctx.Bool("debug") + setting.Log.EnableXORMLog = ctx.Bool("debug") setting.Database.LogSQL = ctx.Bool("debug") - setting.Cfg.Section("log").Key("XORM").SetValue(",") + // FIXME: don't use CfgProvider directly + setting.CfgProvider.Section("log").Key("XORM").SetValue(",") - setting.NewXORMLogService(!ctx.Bool("debug")) + setting.InitSQLLog(!ctx.Bool("debug")) stdCtx, cancel := installSignals() defer cancel() diff --git a/cmd/dump.go b/cmd/dump.go index f40ddbac23..c879d2fbee 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -181,20 +181,22 @@ func runDump(ctx *cli.Context) error { } fileName += "." + outType } - setting.LoadFromExisting() + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() // make sure we are logging to the console no matter what the configuration tells us do to - if _, err := setting.Cfg.Section("log").NewKey("MODE", "console"); err != nil { + // FIXME: don't use CfgProvider directly + if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil { fatal("Setting logging mode to console failed: %v", err) } - if _, err := setting.Cfg.Section("log.console").NewKey("STDERR", "true"); err != nil { + if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil { fatal("Setting console logger to stderr failed: %v", err) } if !setting.InstallLock { log.Error("Is '%s' really the right config path?\n", setting.CustomConf) return fmt.Errorf("gitea is not initialized") } - setting.NewServices() // cannot access session settings otherwise + setting.LoadSettings() // cannot access session settings otherwise stdCtx, cancel := installSignals() defer cancel() @@ -322,7 +324,7 @@ func runDump(ctx *cli.Context) error { log.Info("Packing data directory...%s", setting.AppDataPath) var excludes []string - if setting.Cfg.Section("session").Key("PROVIDER").Value() == "file" { + if setting.SessionConfig.OriginalProvider == "file" { var opts session.Options if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil { return err @@ -339,7 +341,7 @@ func runDump(ctx *cli.Context) error { excludes = append(excludes, setting.LFS.Path) excludes = append(excludes, setting.Attachment.Path) excludes = append(excludes, setting.Packages.Path) - excludes = append(excludes, setting.LogRootPath) + excludes = append(excludes, setting.Log.RootPath) excludes = append(excludes, absFileName) if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil { fatal("Failed to include data directory: %v", err) @@ -378,12 +380,12 @@ func runDump(ctx *cli.Context) error { if ctx.IsSet("skip-log") && ctx.Bool("skip-log") { log.Info("Skip dumping log files") } else { - isExist, err := util.IsExist(setting.LogRootPath) + isExist, err := util.IsExist(setting.Log.RootPath) if err != nil { - log.Error("Unable to check if %s exists. Error: %v", setting.LogRootPath, err) + log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err) } if isExist { - if err := addRecursiveExclude(w, "log", setting.LogRootPath, []string{absFileName}, verbose); err != nil { + if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil { fatal("Failed to include log: %v", err) } } diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index b7b9b3ccc7..0d3970466c 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -94,7 +94,7 @@ func runDumpRepository(ctx *cli.Context) error { log.Info("AppPath: %s", setting.AppPath) log.Info("AppWorkPath: %s", setting.AppWorkPath) log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.LogRootPath) + log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) var ( diff --git a/cmd/embedded.go b/cmd/embedded.go index 118781895e..d87fc0187c 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -112,7 +112,8 @@ func initEmbeddedExtractor(c *cli.Context) error { log.DelNamedLogger(log.DEFAULT) // Read configuration file - setting.LoadAllowEmpty() + setting.InitProviderAllowEmpty() + setting.LoadCommonSettings() pats, err := getPatterns(c.Args()) if err != nil { diff --git a/cmd/mailer.go b/cmd/mailer.go index af6613f159..d05fee12bc 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -17,7 +17,8 @@ func runSendMail(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.LoadFromExisting() + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() if err := argsSet(c, "title"); err != nil { return err diff --git a/cmd/main_test.go b/cmd/main_test.go index 9aacdf7bba..ba323af472 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -12,7 +12,7 @@ import ( func init() { setting.SetCustomPathAndConf("", "", "") - setting.LoadForTest() + setting.InitProviderAndLoadCommonSettingsForTest() } func TestMain(m *testing.M) { diff --git a/cmd/migrate.go b/cmd/migrate.go index 2546fca21d..efa791bc65 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -33,7 +33,7 @@ func runMigrate(ctx *cli.Context) error { log.Info("AppPath: %s", setting.AppPath) log.Info("AppWorkPath: %s", setting.AppWorkPath) log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.LogRootPath) + log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 0b8ebe7c8d..dfa7212e5b 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -136,7 +136,7 @@ func runMigrateStorage(ctx *cli.Context) error { log.Info("AppPath: %s", setting.AppPath) log.Info("AppWorkPath: %s", setting.AppWorkPath) log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.LogRootPath) + log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 23932f821c..c7dff41966 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -54,7 +54,8 @@ func runRestoreRepository(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.LoadFromExisting() + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() var units []string if s := c.String("units"); s != "" { units = strings.Split(s, ",") diff --git a/cmd/serv.go b/cmd/serv.go index 346c918b18..145d1b9e93 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -61,7 +61,8 @@ func setup(logPath string, debug bool) { } else { _ = log.NewLogger(1000, "console", "console", `{"level":"fatal","stacktracelevel":"NONE","stderr":true}`) } - setting.LoadFromExisting() + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() if debug { setting.RunMode = "dev" } diff --git a/cmd/web.go b/cmd/web.go index 49a0335615..8722ddb609 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -158,7 +158,8 @@ func runWeb(ctx *cli.Context) error { log.Info("Global init") // Perform global initialization - setting.LoadFromExisting() + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) // We check that AppDataPath exists here (it should have been created during installation) diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index c35bf9e30f..b63698e62e 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -79,6 +79,10 @@ func main() { Name: "no-xdg-open", Usage: "Set this flag to not use xdg-open to open the PR URL", }, + cli.BoolFlag{ + Name: "continue", + Usage: "Set this flag to continue from a git cherry-pick that has broken", + }, } cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} @@ -104,7 +108,19 @@ func runBackport(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() + continuing := c.Bool("continue") + + var pr string + version := c.String("version") + if version == "" && continuing { + // determine version from current branch name + var err error + pr, version, err = readCurrentBranch(ctx) + if err != nil { + return err + } + } if version == "" { version = readVersion() } @@ -135,13 +151,14 @@ func runBackport(c *cli.Context) error { localReleaseBranch := path.Join(upstream, upstreamReleaseBranch) args := c.Args() - if len(args) == 0 { + if len(args) == 0 && pr == "" { return fmt.Errorf("no PR number provided\nProvide a PR number to backport") - } else if len(args) != 1 { + } else if len(args) != 1 && pr == "" { return fmt.Errorf("multiple PRs provided %v\nOnly a single PR can be backported at a time", args) } - - pr := args[0] + if pr == "" { + pr = args[0] + } backportBranch := c.String("backport-branch") if backportBranch == "" { @@ -168,8 +185,10 @@ func runBackport(c *cli.Context) error { } } - if err := checkoutBackportBranch(ctx, backportBranch, localReleaseBranch); err != nil { - return err + if !continuing { + if err := checkoutBackportBranch(ctx, backportBranch, localReleaseBranch); err != nil { + return err + } } if err := cherrypick(ctx, sha); err != nil { @@ -353,6 +372,22 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro return "", "", fmt.Errorf("unable to find appropriate remote in:\n%s", string(out)) } +func readCurrentBranch(ctx context.Context) (pr, version string, err error) { + out, err := exec.CommandContext(ctx, "git", "branch", "--show-current").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to read current git branch:\n%s\n", string(out)) + return "", "", fmt.Errorf("unable to read current git branch: %w", err) + } + parts := strings.Split(strings.TrimSpace(string(out)), "-") + + if len(parts) != 3 || parts[0] != "backport" { + fmt.Fprintf(os.Stderr, "Unable to continue from git branch:\n%s\n", string(out)) + return "", "", fmt.Errorf("unable to continue from git branch:\n%s", string(out)) + } + + return parts[1], parts[2], nil +} + func readVersion() string { bs, err := os.ReadFile("docs/config.yaml") if err != nil { diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index b31a4a8c68..f12d8a9419 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -49,7 +49,8 @@ func runPR() { log.Fatal(err) } setting.SetCustomPathAndConf("", "", "") - setting.LoadAllowEmpty() + setting.InitProviderAllowEmpty() + setting.LoadCommonSettings() setting.RepoRootPath, err = os.MkdirTemp(os.TempDir(), "repos") if err != nil { @@ -82,7 +83,7 @@ func runPR() { setting.Database.Path = ":memory:" setting.Database.Timeout = 500 */ - dbCfg := setting.Cfg.Section("database") + dbCfg := setting.CfgProvider.Section("database") dbCfg.NewKey("DB_TYPE", "sqlite3") dbCfg.NewKey("PATH", ":memory:") diff --git a/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl index 9559416470..46a397c828 100644 --- a/docker/manifest.rootless.tmpl +++ b/docker/manifest.rootless.tmpl @@ -1,10 +1,12 @@ image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}}-rootless {{#if build.tags}} +{{#unless contains "-rc" build.tag}} tags: {{#each build.tags}} - {{this}}-rootless {{/each}} - "latest-rootless" +{{/unless}} {{/if}} manifests: - diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl index 4cd4ea4ea2..b4ba5a76ed 100644 --- a/docker/manifest.tmpl +++ b/docker/manifest.tmpl @@ -1,10 +1,12 @@ image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}} {{#if build.tags}} +{{#unless contains "-rc" build.tag }} tags: {{#each build.tags}} - {{this}} {{/each}} - "latest" +{{/unless}} {{/if}} manifests: - diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 04344b15dc..36e9919bc7 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -568,7 +568,22 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o - `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server. - `INTERNAL_TOKEN`: **\**: Secret used to validate communication within Gitea binary. - `INTERNAL_TOKEN_URI`: ****: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`) -- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, scrypt, bcrypt\], argon2 will spend more memory than others. +- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, pbkdf2_hi, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory. + - Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended. + - The hash functions may be tuned by using `$` after the algorithm: + - `argon2$