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{}) (int64, 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: [clientTokenAuthorizationHeader: [], 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: [clientTokenAuthorizationHeader: [], 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: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []] // parameters: // - name: body // in: body // description: the user to add // required: true // schema: // $ref: "#/definitions/CreateUserExternal" // 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.CreateUserExternal{} if err := ctx.Bind(&user); err == nil { internal := &model.User{ Name: user.Name, Admin: user.Admin, Pass: password.CreatePassword(user.Pass, a.PasswordStrength), } 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: [clientTokenAuthorizationHeader: [], 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: [clientTokenAuthorizationHeader: [], 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: [clientTokenAuthorizationHeader: [], 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: [clientTokenAuthorizationHeader: [], 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/UpdateUserExternal" // 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.UpdateUserExternal 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 := &model.User{ ID: oldUser.ID, Name: user.Name, Admin: user.Admin, Pass: oldUser.Pass, } if user.Pass != "" { internal.Pass = password.CreatePassword(user.Pass, a.PasswordStrength) } 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 toExternalUser(internal *model.User) *model.UserExternal { return &model.UserExternal{ Name: internal.Name, Admin: internal.Admin, ID: internal.ID, } }