Files
gotify-server/api/user.go
pigpig c172590b92 Add registration
Can be enabled via the registration config flag. (disabled per default)

Fixes gotify/server#395

Co-authored-by: pigpig <pigpig@pig.pig>
Co-authored-by: Karmanyaah Malhotra <32671690+karmanyaahm@users.noreply.github.com>
Co-authored-by: Jannis Mattheis <contact@jmattheis.de>
2021-08-04 19:39:43 +02:00

464 lines
12 KiB
Go

package api
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gotify/server/v2/auth"
"github.com/gotify/server/v2/auth/password"
"github.com/gotify/server/v2/model"
)
// The UserDatabase interface for encapsulating database access.
type UserDatabase interface {
GetUsers() ([]*model.User, error)
GetUserByID(id uint) (*model.User, error)
GetUserByName(name string) (*model.User, error)
DeleteUserByID(id uint) error
UpdateUser(user *model.User) error
CreateUser(user *model.User) error
CountUser(condition ...interface{}) (int, error)
}
// UserChangeNotifier notifies listeners for user changes.
type UserChangeNotifier struct {
userDeletedCallbacks []func(uid uint) error
userAddedCallbacks []func(uid uint) error
}
// OnUserDeleted is called on user deletion.
func (c *UserChangeNotifier) OnUserDeleted(cb func(uid uint) error) {
c.userDeletedCallbacks = append(c.userDeletedCallbacks, cb)
}
// OnUserAdded is called on user creation.
func (c *UserChangeNotifier) OnUserAdded(cb func(uid uint) error) {
c.userAddedCallbacks = append(c.userAddedCallbacks, cb)
}
func (c *UserChangeNotifier) fireUserDeleted(uid uint) error {
for _, cb := range c.userDeletedCallbacks {
if err := cb(uid); err != nil {
return err
}
}
return nil
}
func (c *UserChangeNotifier) fireUserAdded(uid uint) error {
for _, cb := range c.userAddedCallbacks {
if err := cb(uid); err != nil {
return err
}
}
return nil
}
// The UserAPI provides handlers for managing users.
type UserAPI struct {
DB UserDatabase
PasswordStrength int
UserChangeNotifier *UserChangeNotifier
Registration bool
}
// GetUsers returns all the users
// swagger:operation GET /user user getUsers
//
// Return all users.
//
// ---
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// responses:
// 200:
// description: Ok
// schema:
// type: array
// items:
// $ref: "#/definitions/User"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) GetUsers(ctx *gin.Context) {
users, err := a.DB.GetUsers()
if success := successOrAbort(ctx, 500, err); !success {
return
}
var resp []*model.UserExternal
for _, user := range users {
resp = append(resp, toExternalUser(user))
}
ctx.JSON(200, resp)
}
// GetCurrentUser returns the current user
// swagger:operation GET /current/user user currentUser
//
// Return the current user.
//
// ---
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// responses:
// 200:
// description: Ok
// schema:
// $ref: "#/definitions/User"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
user, err := a.DB.GetUserByID(auth.GetUserID(ctx))
if success := successOrAbort(ctx, 500, err); !success {
return
}
ctx.JSON(200, toExternalUser(user))
}
// CreateUser create a user.
// swagger:operation POST /user user createUser
//
// Create a user.
//
// With enabled registration: non admin users can be created without authentication.
// With disabled registrations: users can only be created by admin users.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: body
// in: body
// description: the user to add
// required: true
// schema:
// $ref: "#/definitions/UserWithPass"
// responses:
// 200:
// description: Ok
// schema:
// $ref: "#/definitions/User"
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) CreateUser(ctx *gin.Context) {
user := model.UserExternalWithPass{}
if err := ctx.Bind(&user); err == nil {
internal := a.toInternalUser(&user, []byte{})
existingUser, err := a.DB.GetUserByName(internal.Name)
if success := successOrAbort(ctx, 500, err); !success {
return
}
var requestedBy *model.User
uid := auth.TryGetUserID(ctx)
if uid != nil {
requestedBy, err = a.DB.GetUserByID(*uid)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("could not get user: %s", err))
return
}
}
if requestedBy == nil || !requestedBy.Admin {
status := http.StatusUnauthorized
if requestedBy != nil {
status = http.StatusForbidden
}
if !a.Registration {
ctx.AbortWithError(status, errors.New("you are not allowed to access this api"))
return
}
if internal.Admin {
ctx.AbortWithError(status, errors.New("you are not allowed to create an admin user"))
return
}
}
if existingUser == nil {
if success := successOrAbort(ctx, 500, a.DB.CreateUser(internal)); !success {
return
}
if err := a.UserChangeNotifier.fireUserAdded(internal.ID); err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, toExternalUser(internal))
} else {
ctx.AbortWithError(400, errors.New("username already exists"))
}
}
}
// GetUserByID returns the user by id
// swagger:operation GET /user/{id} user getUser
//
// Get a user.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: id
// in: path
// description: the user id
// required: true
// type: integer
// format: int64
// responses:
// 200:
// description: Ok
// schema:
// $ref: "#/definitions/User"
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
// 404:
// description: Not Found
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) GetUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
user, err := a.DB.GetUserByID(id)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if user != nil {
ctx.JSON(200, toExternalUser(user))
} else {
ctx.AbortWithError(404, errors.New("user does not exist"))
}
})
}
// DeleteUserByID deletes the user by id
// swagger:operation DELETE /user/{id} user deleteUser
//
// Deletes a user.
//
// ---
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: id
// in: path
// description: the user id
// required: true
// type: integer
// format: int64
// responses:
// 200:
// description: Ok
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
// 404:
// description: Not Found
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
user, err := a.DB.GetUserByID(id)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if user != nil {
adminCount, err := a.DB.CountUser(&model.User{Admin: true})
if success := successOrAbort(ctx, 500, err); !success {
return
}
if user.Admin && adminCount == 1 {
ctx.AbortWithError(400, errors.New("cannot delete last admin"))
return
}
if err := a.UserChangeNotifier.fireUserDeleted(id); err != nil {
ctx.AbortWithError(500, err)
return
}
successOrAbort(ctx, 500, a.DB.DeleteUserByID(id))
} else {
ctx.AbortWithError(404, errors.New("user does not exist"))
}
})
}
// ChangePassword changes the password from the current user
// swagger:operation POST /current/user/password user updateCurrentUser
//
// Update the password of the current user.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: body
// in: body
// description: the user
// required: true
// schema:
// $ref: "#/definitions/UserPass"
// responses:
// 200:
// description: Ok
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) ChangePassword(ctx *gin.Context) {
pw := model.UserExternalPass{}
if err := ctx.Bind(&pw); err == nil {
user, err := a.DB.GetUserByID(auth.GetUserID(ctx))
if success := successOrAbort(ctx, 500, err); !success {
return
}
user.Pass = password.CreatePassword(pw.Pass, a.PasswordStrength)
successOrAbort(ctx, 500, a.DB.UpdateUser(user))
}
}
// UpdateUserByID updates and user by id
// swagger:operation POST /user/{id} user updateUser
//
// Update a user.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: id
// in: path
// description: the user id
// required: true
// type: integer
// format: int64
// - name: body
// in: body
// description: the updated user
// required: true
// schema:
// $ref: "#/definitions/UserWithPass"
// responses:
// 200:
// description: Ok
// schema:
// $ref: "#/definitions/User"
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
// 404:
// description: Not Found
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) UpdateUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
var user *model.UserExternalWithPass
if err := ctx.Bind(&user); err == nil {
oldUser, err := a.DB.GetUserByID(id)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if oldUser != nil {
adminCount, err := a.DB.CountUser(&model.User{Admin: true})
if success := successOrAbort(ctx, 500, err); !success {
return
}
if !user.Admin && oldUser.Admin && adminCount == 1 {
ctx.AbortWithError(400, errors.New("cannot delete last admin"))
return
}
internal := a.toInternalUser(user, oldUser.Pass)
internal.ID = id
if success := successOrAbort(ctx, 500, a.DB.UpdateUser(internal)); !success {
return
}
ctx.JSON(200, toExternalUser(internal))
} else {
ctx.AbortWithError(404, errors.New("user does not exist"))
}
}
})
}
func (a *UserAPI) toInternalUser(response *model.UserExternalWithPass, pw []byte) *model.User {
user := &model.User{
Name: response.Name,
Admin: response.Admin,
}
if response.Pass != "" {
user.Pass = password.CreatePassword(response.Pass, a.PasswordStrength)
} else {
user.Pass = pw
}
return user
}
func toExternalUser(internal *model.User) *model.UserExternal {
return &model.UserExternal{
Name: internal.Name,
Admin: internal.Admin,
ID: internal.ID,
}
}