2015-12-05 17:13:13 -05:00
|
|
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
2019-01-23 17:30:19 -05:00
|
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
2022-11-27 13:20:29 -05:00
|
|
|
// SPDX-License-Identifier: MIT
|
2015-12-05 17:13:13 -05:00
|
|
|
|
|
|
|
package admin
|
|
|
|
|
|
|
|
import (
|
2019-10-14 11:24:26 -04:00
|
|
|
"errors"
|
2020-02-03 11:46:33 -05:00
|
|
|
"fmt"
|
2019-12-20 12:07:12 -05:00
|
|
|
"net/http"
|
2021-11-25 20:56:16 -05:00
|
|
|
"strings"
|
2019-10-14 11:24:26 -04:00
|
|
|
|
2016-11-10 11:24:48 -05:00
|
|
|
"code.gitea.io/gitea/models"
|
2021-12-10 03:14:24 -05:00
|
|
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
2022-01-02 08:12:35 -05:00
|
|
|
"code.gitea.io/gitea/models/auth"
|
2021-11-24 04:49:20 -05:00
|
|
|
"code.gitea.io/gitea/models/db"
|
2021-11-11 02:03:30 -05:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2023-02-19 02:35:20 -05:00
|
|
|
"code.gitea.io/gitea/modules/auth/password"
|
2016-11-10 11:24:48 -05:00
|
|
|
"code.gitea.io/gitea/modules/context"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
2021-12-16 21:03:39 -05:00
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2019-05-11 06:21:34 -04:00
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
2023-02-16 11:32:01 -05:00
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
2022-04-29 15:38:11 -04:00
|
|
|
"code.gitea.io/gitea/modules/util"
|
2021-01-26 10:36:53 -05:00
|
|
|
"code.gitea.io/gitea/modules/web"
|
2016-11-10 11:24:48 -05:00
|
|
|
"code.gitea.io/gitea/routers/api/v1/user"
|
2020-01-24 14:00:29 -05:00
|
|
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
2021-12-10 03:14:24 -05:00
|
|
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
2022-12-28 21:57:15 -05:00
|
|
|
"code.gitea.io/gitea/services/convert"
|
2019-09-24 01:02:49 -04:00
|
|
|
"code.gitea.io/gitea/services/mailer"
|
2021-11-18 12:42:27 -05:00
|
|
|
user_service "code.gitea.io/gitea/services/user"
|
2015-12-05 17:13:13 -05:00
|
|
|
)
|
|
|
|
|
2022-01-02 08:12:35 -05:00
|
|
|
func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64, loginName string) {
|
2015-12-05 17:13:13 -05:00
|
|
|
if sourceID == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-11 00:24:07 -04:00
|
|
|
source, err := auth.GetSourceByID(ctx, sourceID)
|
2015-12-05 17:13:13 -05:00
|
|
|
if err != nil {
|
2022-01-02 08:12:35 -05:00
|
|
|
if auth.IsErrSourceNotExist(err) {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
} else {
|
2022-01-02 08:12:35 -05:00
|
|
|
ctx.Error(http.StatusInternalServerError, "auth.GetSourceByID", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
u.LoginType = source.Type
|
|
|
|
u.LoginSource = source.ID
|
|
|
|
u.LoginName = loginName
|
|
|
|
}
|
|
|
|
|
2017-11-13 02:02:25 -05:00
|
|
|
// CreateUser create a user
|
2021-01-26 10:36:53 -05:00
|
|
|
func CreateUser(ctx *context.APIContext) {
|
2017-11-13 02:02:25 -05:00
|
|
|
// swagger:operation POST /admin/users admin adminCreateUser
|
|
|
|
// ---
|
|
|
|
// summary: Create a user
|
|
|
|
// consumes:
|
|
|
|
// - application/json
|
|
|
|
// produces:
|
|
|
|
// - application/json
|
|
|
|
// parameters:
|
|
|
|
// - name: body
|
|
|
|
// in: body
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/CreateUserOption"
|
|
|
|
// responses:
|
|
|
|
// "201":
|
|
|
|
// "$ref": "#/responses/User"
|
2019-12-20 12:07:12 -05:00
|
|
|
// "400":
|
|
|
|
// "$ref": "#/responses/error"
|
2020-01-09 06:56:32 -05:00
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
2017-11-13 02:02:25 -05:00
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
2022-03-26 05:04:22 -04:00
|
|
|
|
2021-01-26 10:36:53 -05:00
|
|
|
form := web.GetForm(ctx).(*api.CreateUserOption)
|
2021-06-26 15:53:14 -04:00
|
|
|
|
2021-11-24 04:49:20 -05:00
|
|
|
u := &user_model.User{
|
2019-02-27 14:37:57 -05:00
|
|
|
Name: form.Username,
|
|
|
|
FullName: form.FullName,
|
|
|
|
Email: form.Email,
|
|
|
|
Passwd: form.Password,
|
|
|
|
MustChangePassword: true,
|
2022-01-02 08:12:35 -05:00
|
|
|
LoginType: auth.Plain,
|
2019-02-27 14:37:57 -05:00
|
|
|
}
|
|
|
|
if form.MustChangePassword != nil {
|
|
|
|
u.MustChangePassword = *form.MustChangePassword
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
|
2022-01-02 08:12:35 -05:00
|
|
|
parseAuthSource(ctx, u, form.SourceID, form.LoginName)
|
2015-12-05 17:13:13 -05:00
|
|
|
if ctx.Written() {
|
|
|
|
return
|
|
|
|
}
|
2023-12-18 21:32:45 -05:00
|
|
|
|
|
|
|
if u.LoginType == auth.Plain {
|
|
|
|
if len(form.Password) < setting.MinPasswordLength {
|
|
|
|
err := errors.New("PasswordIsRequired")
|
|
|
|
ctx.Error(http.StatusBadRequest, "PasswordIsRequired", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !password.IsComplexEnough(form.Password) {
|
|
|
|
err := errors.New("PasswordComplexity")
|
|
|
|
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
pwned, err := password.IsPwned(ctx, form.Password)
|
|
|
|
if pwned {
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err.Error())
|
|
|
|
}
|
|
|
|
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
|
|
|
return
|
2020-09-08 18:06:39 -04:00
|
|
|
}
|
|
|
|
}
|
2021-06-26 15:53:14 -04:00
|
|
|
|
2022-04-29 15:38:11 -04:00
|
|
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
|
|
|
IsActive: util.OptionalBoolTrue,
|
|
|
|
}
|
|
|
|
|
|
|
|
if form.Restricted != nil {
|
|
|
|
overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
|
|
|
|
}
|
|
|
|
|
2021-06-26 15:53:14 -04:00
|
|
|
if form.Visibility != "" {
|
2022-04-29 15:38:11 -04:00
|
|
|
visibility := api.VisibilityModes[form.Visibility]
|
|
|
|
overwriteDefault.Visibility = &visibility
|
2021-06-26 15:53:14 -04:00
|
|
|
}
|
|
|
|
|
2023-02-16 11:32:01 -05:00
|
|
|
// Update the user creation timestamp. This can only be done after the user
|
|
|
|
// record has been inserted into the database; the insert intself will always
|
|
|
|
// set the creation timestamp to "now".
|
|
|
|
if form.Created != nil {
|
|
|
|
u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
|
|
|
|
u.UpdatedUnix = u.CreatedUnix
|
|
|
|
}
|
|
|
|
|
2023-09-14 13:09:32 -04:00
|
|
|
if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
|
2021-11-24 04:49:20 -05:00
|
|
|
if user_model.IsErrUserAlreadyExist(err) ||
|
2021-11-11 02:03:30 -05:00
|
|
|
user_model.IsErrEmailAlreadyUsed(err) ||
|
2021-11-24 04:49:20 -05:00
|
|
|
db.IsErrNameReserved(err) ||
|
|
|
|
db.IsErrNameCharsNotAllowed(err) ||
|
2022-03-14 13:39:54 -04:00
|
|
|
user_model.IsErrEmailCharIsNotSupported(err) ||
|
2021-11-11 02:03:30 -05:00
|
|
|
user_model.IsErrEmailInvalid(err) ||
|
2021-11-24 04:49:20 -05:00
|
|
|
db.IsErrNamePatternNotAllowed(err) {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
} else {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusInternalServerError, "CreateUser", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2022-03-22 03:03:22 -04:00
|
|
|
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
2015-12-05 17:13:13 -05:00
|
|
|
|
2016-07-15 12:36:39 -04:00
|
|
|
// Send email notification.
|
2019-09-24 01:02:49 -04:00
|
|
|
if form.SendNotify {
|
2021-04-02 06:25:13 -04:00
|
|
|
mailer.SendRegisterNotifyMail(u)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 08:37:34 -05:00
|
|
|
ctx.JSON(http.StatusCreated, convert.ToUser(ctx, u, ctx.Doer))
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
|
2016-11-24 02:04:31 -05:00
|
|
|
// EditUser api for modifying a user's information
|
2021-01-26 10:36:53 -05:00
|
|
|
func EditUser(ctx *context.APIContext) {
|
2017-11-13 02:02:25 -05:00
|
|
|
// swagger:operation PATCH /admin/users/{username} admin adminEditUser
|
|
|
|
// ---
|
|
|
|
// summary: Edit an existing user
|
|
|
|
// consumes:
|
|
|
|
// - application/json
|
|
|
|
// produces:
|
|
|
|
// - application/json
|
|
|
|
// parameters:
|
|
|
|
// - name: username
|
|
|
|
// in: path
|
|
|
|
// description: username of user to edit
|
|
|
|
// type: string
|
|
|
|
// required: true
|
|
|
|
// - name: body
|
|
|
|
// in: body
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/EditUserOption"
|
|
|
|
// responses:
|
|
|
|
// "200":
|
|
|
|
// "$ref": "#/responses/User"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
2022-03-26 05:04:22 -04:00
|
|
|
|
2021-01-26 10:36:53 -05:00
|
|
|
form := web.GetForm(ctx).(*api.EditUserOption)
|
2015-12-05 17:13:13 -05:00
|
|
|
|
2022-03-26 05:04:22 -04:00
|
|
|
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
|
2015-12-05 17:13:13 -05:00
|
|
|
if ctx.Written() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-19 20:56:42 -05:00
|
|
|
if len(form.Password) != 0 {
|
2021-12-16 21:03:39 -05:00
|
|
|
if len(form.Password) < setting.MinPasswordLength {
|
|
|
|
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
|
|
|
|
return
|
|
|
|
}
|
2019-10-14 11:24:26 -04:00
|
|
|
if !password.IsComplexEnough(form.Password) {
|
|
|
|
err := errors.New("PasswordComplexity")
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
2019-10-14 11:24:26 -04:00
|
|
|
return
|
|
|
|
}
|
2021-05-31 02:18:11 -04:00
|
|
|
pwned, err := password.IsPwned(ctx, form.Password)
|
2020-09-08 18:06:39 -04:00
|
|
|
if pwned {
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err.Error())
|
|
|
|
}
|
|
|
|
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
|
|
|
return
|
|
|
|
}
|
2022-03-26 05:04:22 -04:00
|
|
|
if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
2016-12-20 07:32:02 -05:00
|
|
|
return
|
|
|
|
}
|
2022-03-26 05:04:22 -04:00
|
|
|
if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
|
2021-01-10 13:05:18 -05:00
|
|
|
ctx.InternalServerError(err)
|
|
|
|
return
|
|
|
|
}
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
|
2019-02-27 14:37:57 -05:00
|
|
|
if form.MustChangePassword != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.MustChangePassword = *form.MustChangePassword
|
2019-02-27 14:37:57 -05:00
|
|
|
}
|
|
|
|
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.LoginName = form.LoginName
|
2020-11-19 20:56:42 -05:00
|
|
|
|
|
|
|
if form.FullName != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.FullName = *form.FullName
|
2020-11-19 20:56:42 -05:00
|
|
|
}
|
2021-11-25 20:56:16 -05:00
|
|
|
var emailChanged bool
|
2020-11-19 20:56:42 -05:00
|
|
|
if form.Email != nil {
|
2021-11-25 20:56:16 -05:00
|
|
|
email := strings.TrimSpace(*form.Email)
|
|
|
|
if len(email) == 0 {
|
2020-11-19 20:56:42 -05:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string"))
|
|
|
|
return
|
|
|
|
}
|
2021-11-25 20:56:16 -05:00
|
|
|
|
|
|
|
if err := user_model.ValidateEmail(email); err != nil {
|
|
|
|
ctx.InternalServerError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-26 05:04:22 -04:00
|
|
|
emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
|
|
|
|
ctx.ContextUser.Email = email
|
2020-11-19 20:56:42 -05:00
|
|
|
}
|
|
|
|
if form.Website != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.Website = *form.Website
|
2020-11-19 20:56:42 -05:00
|
|
|
}
|
|
|
|
if form.Location != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.Location = *form.Location
|
2020-11-19 20:56:42 -05:00
|
|
|
}
|
2021-05-02 15:03:15 -04:00
|
|
|
if form.Description != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.Description = *form.Description
|
2021-05-02 15:03:15 -04:00
|
|
|
}
|
2015-12-05 17:13:13 -05:00
|
|
|
if form.Active != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.IsActive = *form.Active
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
2021-06-26 15:53:14 -04:00
|
|
|
if len(form.Visibility) != 0 {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
|
2021-06-26 15:53:14 -04:00
|
|
|
}
|
2015-12-05 17:13:13 -05:00
|
|
|
if form.Admin != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.IsAdmin = *form.Admin
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
if form.AllowGitHook != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.AllowGitHook = *form.AllowGitHook
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
if form.AllowImportLocal != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
2016-08-11 14:49:31 -04:00
|
|
|
if form.MaxRepoCreation != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
|
2016-08-11 14:49:31 -04:00
|
|
|
}
|
2018-08-23 19:59:47 -04:00
|
|
|
if form.AllowCreateOrganization != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
|
2018-08-23 19:59:47 -04:00
|
|
|
}
|
|
|
|
if form.ProhibitLogin != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
|
2018-08-23 19:59:47 -04:00
|
|
|
}
|
2021-02-18 03:25:35 -05:00
|
|
|
if form.Restricted != nil {
|
2022-03-26 05:04:22 -04:00
|
|
|
ctx.ContextUser.IsRestricted = *form.Restricted
|
2021-02-18 03:25:35 -05:00
|
|
|
}
|
2015-12-05 17:13:13 -05:00
|
|
|
|
2022-05-20 10:08:52 -04:00
|
|
|
if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
|
2022-03-14 13:39:54 -04:00
|
|
|
if user_model.IsErrEmailAlreadyUsed(err) ||
|
|
|
|
user_model.IsErrEmailCharIsNotSupported(err) ||
|
|
|
|
user_model.IsErrEmailInvalid(err) {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
} else {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2022-03-26 05:04:22 -04:00
|
|
|
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
2015-12-05 17:13:13 -05:00
|
|
|
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 08:37:34 -05:00
|
|
|
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
|
2016-11-24 02:04:31 -05:00
|
|
|
// DeleteUser api for deleting a user
|
2016-03-13 18:49:16 -04:00
|
|
|
func DeleteUser(ctx *context.APIContext) {
|
2017-11-13 02:02:25 -05:00
|
|
|
// swagger:operation DELETE /admin/users/{username} admin adminDeleteUser
|
|
|
|
// ---
|
|
|
|
// summary: Delete a user
|
|
|
|
// produces:
|
|
|
|
// - application/json
|
|
|
|
// parameters:
|
|
|
|
// - name: username
|
|
|
|
// in: path
|
|
|
|
// description: username of user to delete
|
|
|
|
// type: string
|
|
|
|
// required: true
|
2023-03-13 15:41:38 -04:00
|
|
|
// - name: purge
|
|
|
|
// in: query
|
|
|
|
// description: purge the user from the system completely
|
|
|
|
// type: boolean
|
2017-11-13 02:02:25 -05:00
|
|
|
// responses:
|
|
|
|
// "204":
|
|
|
|
// "$ref": "#/responses/empty"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
2023-07-24 04:48:44 -04:00
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
2017-11-13 02:02:25 -05:00
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
2019-12-20 12:07:12 -05:00
|
|
|
|
2022-03-26 05:04:22 -04:00
|
|
|
if ctx.ContextUser.IsOrganization() {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
|
2020-02-03 11:46:33 -05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-08 16:22:55 -04:00
|
|
|
// admin should not delete themself
|
|
|
|
if ctx.ContextUser.ID == ctx.Doer.ID {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-14 03:22:09 -04:00
|
|
|
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
|
2015-12-05 17:13:13 -05:00
|
|
|
if models.IsErrUserOwnRepos(err) ||
|
2022-03-30 04:42:47 -04:00
|
|
|
models.IsErrUserHasOrgs(err) ||
|
|
|
|
models.IsErrUserOwnPackages(err) {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
} else {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusInternalServerError, "DeleteUser", err)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2022-03-26 05:04:22 -04:00
|
|
|
log.Trace("Account deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
2015-12-05 17:13:13 -05:00
|
|
|
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Status(http.StatusNoContent)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
|
|
|
|
2016-11-24 02:04:31 -05:00
|
|
|
// CreatePublicKey api for creating a public key to a user
|
2021-01-26 10:36:53 -05:00
|
|
|
func CreatePublicKey(ctx *context.APIContext) {
|
2017-11-13 02:02:25 -05:00
|
|
|
// swagger:operation POST /admin/users/{username}/keys admin adminCreatePublicKey
|
|
|
|
// ---
|
|
|
|
// summary: Add a public key on behalf of a user
|
|
|
|
// consumes:
|
|
|
|
// - application/json
|
|
|
|
// produces:
|
|
|
|
// - application/json
|
|
|
|
// parameters:
|
|
|
|
// - name: username
|
|
|
|
// in: path
|
|
|
|
// description: username of the user
|
|
|
|
// type: string
|
|
|
|
// required: true
|
2018-10-20 23:40:42 -04:00
|
|
|
// - name: key
|
|
|
|
// in: body
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/CreateKeyOption"
|
2017-11-13 02:02:25 -05:00
|
|
|
// responses:
|
|
|
|
// "201":
|
|
|
|
// "$ref": "#/responses/PublicKey"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
2022-03-26 05:04:22 -04:00
|
|
|
|
2021-01-26 10:36:53 -05:00
|
|
|
form := web.GetForm(ctx).(*api.CreateKeyOption)
|
2022-03-26 05:04:22 -04:00
|
|
|
|
|
|
|
user.CreateUserPublicKey(ctx, *form, ctx.ContextUser.ID)
|
2015-12-05 17:13:13 -05:00
|
|
|
}
|
2017-12-06 05:27:10 -05:00
|
|
|
|
|
|
|
// DeleteUserPublicKey api for deleting a user's public key
|
|
|
|
func DeleteUserPublicKey(ctx *context.APIContext) {
|
|
|
|
// swagger:operation DELETE /admin/users/{username}/keys/{id} admin adminDeleteUserPublicKey
|
|
|
|
// ---
|
|
|
|
// summary: Delete a user's public key
|
|
|
|
// produces:
|
|
|
|
// - application/json
|
|
|
|
// parameters:
|
|
|
|
// - name: username
|
|
|
|
// in: path
|
|
|
|
// description: username of user
|
|
|
|
// type: string
|
|
|
|
// required: true
|
|
|
|
// - name: id
|
|
|
|
// in: path
|
|
|
|
// description: id of the key to delete
|
|
|
|
// type: integer
|
2018-10-20 23:40:42 -04:00
|
|
|
// format: int64
|
2017-12-06 05:27:10 -05:00
|
|
|
// required: true
|
|
|
|
// responses:
|
|
|
|
// "204":
|
|
|
|
// "$ref": "#/responses/empty"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
2019-12-20 12:07:12 -05:00
|
|
|
|
2023-09-25 09:17:37 -04:00
|
|
|
if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.ParamsInt64(":id")); err != nil {
|
2021-12-10 03:14:24 -05:00
|
|
|
if asymkey_model.IsErrKeyNotExist(err) {
|
2019-03-18 22:29:43 -04:00
|
|
|
ctx.NotFound()
|
2021-12-10 03:14:24 -05:00
|
|
|
} else if asymkey_model.IsErrKeyAccessDenied(err) {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
|
2017-12-06 05:27:10 -05:00
|
|
|
} else {
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Error(http.StatusInternalServerError, "DeleteUserPublicKey", err)
|
2017-12-06 05:27:10 -05:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2022-03-26 05:04:22 -04:00
|
|
|
log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
2017-12-06 05:27:10 -05:00
|
|
|
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.Status(http.StatusNoContent)
|
2017-12-06 05:27:10 -05:00
|
|
|
}
|
2019-01-23 17:30:19 -05:00
|
|
|
|
2023-03-15 07:53:01 -04:00
|
|
|
// SearchUsers API for getting information of the users according the filter conditions
|
|
|
|
func SearchUsers(ctx *context.APIContext) {
|
|
|
|
// swagger:operation GET /admin/users admin adminSearchUsers
|
2019-01-23 17:30:19 -05:00
|
|
|
// ---
|
2023-03-15 07:53:01 -04:00
|
|
|
// summary: Search users according filter conditions
|
2019-01-23 17:30:19 -05:00
|
|
|
// produces:
|
|
|
|
// - application/json
|
2020-01-24 14:00:29 -05:00
|
|
|
// parameters:
|
2023-03-15 07:53:01 -04:00
|
|
|
// - name: source_id
|
|
|
|
// in: query
|
|
|
|
// description: ID of the user's login source to search for
|
|
|
|
// type: integer
|
|
|
|
// format: int64
|
|
|
|
// - name: login_name
|
|
|
|
// in: query
|
|
|
|
// description: user's login name to search for
|
|
|
|
// type: string
|
2020-01-24 14:00:29 -05:00
|
|
|
// - name: page
|
|
|
|
// in: query
|
|
|
|
// description: page number of results to return (1-based)
|
|
|
|
// type: integer
|
|
|
|
// - name: limit
|
|
|
|
// in: query
|
2020-06-09 00:57:38 -04:00
|
|
|
// description: page size of results
|
2020-01-24 14:00:29 -05:00
|
|
|
// type: integer
|
2019-01-23 17:30:19 -05:00
|
|
|
// responses:
|
|
|
|
// "200":
|
|
|
|
// "$ref": "#/responses/UserList"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
2019-12-20 12:07:12 -05:00
|
|
|
|
2020-06-21 04:22:06 -04:00
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
|
2023-09-14 13:09:32 -04:00
|
|
|
users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
|
2022-03-22 03:03:22 -04:00
|
|
|
Actor: ctx.Doer,
|
2021-11-24 04:49:20 -05:00
|
|
|
Type: user_model.UserTypeIndividual,
|
2023-03-15 07:53:01 -04:00
|
|
|
LoginName: ctx.FormTrim("login_name"),
|
|
|
|
SourceID: ctx.FormInt64("source_id"),
|
2021-11-24 04:49:20 -05:00
|
|
|
OrderBy: db.SearchOrderByAlphabetically,
|
2020-06-21 04:22:06 -04:00
|
|
|
ListOptions: listOptions,
|
2019-01-23 17:30:19 -05:00
|
|
|
})
|
|
|
|
if err != nil {
|
2023-03-15 07:53:01 -04:00
|
|
|
ctx.Error(http.StatusInternalServerError, "SearchUsers", err)
|
2019-01-23 17:30:19 -05:00
|
|
|
return
|
|
|
|
}
|
2019-04-15 12:36:59 -04:00
|
|
|
|
|
|
|
results := make([]*api.User, len(users))
|
|
|
|
for i := range users {
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 08:37:34 -05:00
|
|
|
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
|
2019-04-15 12:36:59 -04:00
|
|
|
}
|
|
|
|
|
2020-06-21 04:22:06 -04:00
|
|
|
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
2021-08-12 08:43:08 -04:00
|
|
|
ctx.SetTotalCountHeader(maxResults)
|
2019-12-20 12:07:12 -05:00
|
|
|
ctx.JSON(http.StatusOK, &results)
|
2019-01-23 17:30:19 -05:00
|
|
|
}
|
2023-03-14 03:45:21 -04:00
|
|
|
|
|
|
|
// RenameUser api for renaming a user
|
|
|
|
func RenameUser(ctx *context.APIContext) {
|
|
|
|
// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
|
|
|
|
// ---
|
|
|
|
// summary: Rename a user
|
|
|
|
// produces:
|
|
|
|
// - application/json
|
|
|
|
// parameters:
|
|
|
|
// - name: username
|
|
|
|
// in: path
|
|
|
|
// description: existing username of user
|
|
|
|
// type: string
|
|
|
|
// required: true
|
|
|
|
// - name: body
|
|
|
|
// in: body
|
|
|
|
// required: true
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/RenameUserOption"
|
|
|
|
// responses:
|
|
|
|
// "204":
|
|
|
|
// "$ref": "#/responses/empty"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
|
|
|
|
if ctx.ContextUser.IsOrganization() {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-05-21 11:13:47 -04:00
|
|
|
oldName := ctx.ContextUser.Name
|
2023-03-14 03:45:21 -04:00
|
|
|
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 {
|
|
|
|
switch {
|
2023-05-21 11:13:47 -04:00
|
|
|
case user_model.IsErrUsernameNotChanged(err):
|
|
|
|
// Noop as username is not changed
|
|
|
|
ctx.Status(http.StatusNoContent)
|
2023-03-14 03:45:21 -04:00
|
|
|
case user_model.IsErrUserAlreadyExist(err):
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
|
|
|
|
case db.IsErrNameReserved(err):
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
|
|
|
|
case db.IsErrNamePatternNotAllowed(err):
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
|
|
|
|
case db.IsErrNameCharsNotAllowed(err):
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
|
|
|
|
default:
|
|
|
|
ctx.ServerError("ChangeUserName", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2023-05-21 11:13:47 -04:00
|
|
|
|
|
|
|
log.Trace("User name changed: %s -> %s", oldName, newName)
|
|
|
|
ctx.Status(http.StatusOK)
|
2023-03-14 03:45:21 -04:00
|
|
|
}
|