Skip to content

Commit

Permalink
Add registration
Browse files Browse the repository at this point in the history
Can be enabled via the registration config flag. (disabled per default)

Fixes gotify#395

Co-authored-by: pigpig <[email protected]>
Co-authored-by: Karmanyaah Malhotra <[email protected]>
Co-authored-by: Jannis Mattheis <[email protected]>
  • Loading branch information
3 people committed Aug 4, 2021
1 parent 7e261be commit db864e0
Show file tree
Hide file tree
Showing 16 changed files with 391 additions and 20 deletions.
2 changes: 1 addition & 1 deletion api/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,6 @@ func fakeImage(t *testing.T, path string) {
data, err := ioutil.ReadFile("../test/assets/image.png")
assert.Nil(t, err)
// Write data to dst
err = ioutil.WriteFile(path, data, 0644)
err = ioutil.WriteFile(path, data, 0o644)
assert.Nil(t, err)
}
34 changes: 33 additions & 1 deletion api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package api

import (
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/gotify/server/v2/auth"
Expand Down Expand Up @@ -59,6 +61,7 @@ type UserAPI struct {
DB UserDatabase
PasswordStrength int
UserChangeNotifier *UserChangeNotifier
Registration bool
}

// GetUsers returns all the users
Expand Down Expand Up @@ -126,11 +129,14 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
ctx.JSON(200, toExternalUser(user))
}

// CreateUser creates a 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]
Expand Down Expand Up @@ -167,6 +173,32 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) {
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
Expand Down
111 changes: 108 additions & 3 deletions api/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/gin-gonic/gin"
"github.com/gotify/server/v2/auth"
"github.com/gotify/server/v2/auth/password"
"github.com/gotify/server/v2/mode"
"github.com/gotify/server/v2/model"
Expand Down Expand Up @@ -35,7 +36,9 @@ func (s *UserSuite) BeforeTest(suiteName, testName string) {
mode.Set(mode.TestDev)
s.recorder = httptest.NewRecorder()
s.ctx, _ = gin.CreateTestContext(s.recorder)

s.db = testdb.NewDB(s.T())

s.notifier = new(UserChangeNotifier)
s.notifier.OnUserDeleted(func(uint) error {
s.notifiedDelete = true
Expand Down Expand Up @@ -164,15 +167,17 @@ func (s *UserSuite) Test_DeleteUserByID_NotifyFail() {
}

func (s *UserSuite) Test_CreateUser() {
s.loginAdmin()

assert.False(s.T(), s.notifiedAdd)
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

user := &model.UserExternal{ID: 1, Name: "tom", Admin: true}
test.BodyEquals(s.T(), user, s.recorder)
assert.Equal(s.T(), 200, s.recorder.Code)
user := &model.UserExternal{ID: 2, Name: "tom", Admin: true}
test.BodyEquals(s.T(), user, s.recorder)

if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
assert.NotNil(s.T(), created)
Expand All @@ -181,7 +186,88 @@ func (s *UserSuite) Test_CreateUser() {
assert.True(s.T(), s.notifiedAdd)
}

func (s *UserSuite) Test_CreateUser_ByNonAdmin() {
s.loginUser()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

assert.Equal(s.T(), 403, s.recorder.Code)
}

func (s *UserSuite) Test_CreateUser_Register_ByNonAdmin() {
s.loginUser()
s.a.Registration = true

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

assert.Equal(s.T(), 200, s.recorder.Code)
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
assert.NotNil(s.T(), created)
}
}

func (s *UserSuite) Test_CreateUser_Register_Admin_ByNonAdmin() {
s.a.Registration = true
s.loginUser()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

assert.Equal(s.T(), 403, s.recorder.Code)
s.db.AssertUsernameNotExist("tom")
}

func (s *UserSuite) Test_CreateUser_Anonymous() {
s.noLogin()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

assert.Equal(s.T(), 401, s.recorder.Code)
s.db.AssertUsernameNotExist("tom")
}

func (s *UserSuite) Test_CreateUser_Register_Anonymous() {
s.a.Registration = true
s.noLogin()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

assert.Equal(s.T(), 200, s.recorder.Code)
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
assert.NotNil(s.T(), created)
}
}

func (s *UserSuite) Test_CreateUser_Register_Admin_Anonymous() {
s.a.Registration = true
s.noLogin()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateUser(s.ctx)

assert.Equal(s.T(), 401, s.recorder.Code)
s.db.AssertUsernameNotExist("tom")
}

func (s *UserSuite) Test_CreateUser_NotifyFail() {
s.loginAdmin()

s.notifier.OnUserAdded(func(id uint) error {
user, err := s.db.GetUserByID(id)
if err != nil {
Expand All @@ -201,6 +287,8 @@ func (s *UserSuite) Test_CreateUser_NotifyFail() {
}

func (s *UserSuite) Test_CreateUser_NoPassword() {
s.loginAdmin()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

Expand All @@ -210,6 +298,8 @@ func (s *UserSuite) Test_CreateUser_NoPassword() {
}

func (s *UserSuite) Test_CreateUser_NoName() {
s.loginAdmin()

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "", "pass": "asd", "admin": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

Expand All @@ -219,7 +309,8 @@ func (s *UserSuite) Test_CreateUser_NoName() {
}

func (s *UserSuite) Test_CreateUser_NameAlreadyExists() {
s.db.NewUserWithName(1, "tom")
s.loginAdmin()
s.db.NewUserWithName(2, "tom")

s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")
Expand Down Expand Up @@ -333,6 +424,20 @@ func (s *UserSuite) Test_UpdatePassword_EmptyPassword() {
assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
}

func (s *UserSuite) loginAdmin() {
s.db.CreateUser(&model.User{ID: 1, Name: "admin", Admin: true})
auth.RegisterAuthentication(s.ctx, nil, 1, "")
}

func (s *UserSuite) loginUser() {
s.db.CreateUser(&model.User{ID: 1, Name: "user", Admin: false})
auth.RegisterAuthentication(s.ctx, nil, 1, "")
}

func (s *UserSuite) noLogin() {
auth.RegisterAuthentication(s.ctx, nil, 0, "")
}

func externalOf(user *model.User) *model.UserExternal {
return &model.UserExternal{Name: user.Name, Admin: user.Admin, ID: user.ID}
}
26 changes: 26 additions & 0 deletions auth/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,29 @@ func (a *Auth) requireToken(auth authenticate) gin.HandlerFunc {
ctx.AbortWithError(401, errors.New("you need to provide a valid access token or user credentials to access this api"))
}
}

func (a *Auth) Optional() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := a.tokenFromQueryOrHeader(ctx)
user, err := a.userFromBasicAuth(ctx)
if err != nil {
RegisterAuthentication(ctx, nil, 0, "")
ctx.Next()
return
}

if user != nil {
RegisterAuthentication(ctx, user, user.ID, token)
ctx.Next()
return
} else if token != "" {
if tokenClient, err := a.DB.GetClientByToken(token); err == nil && tokenClient != nil {
RegisterAuthentication(ctx, user, tokenClient.UserID, token)
ctx.Next()
return
}
}
RegisterAuthentication(ctx, nil, 0, "")
ctx.Next()
}
}
38 changes: 34 additions & 4 deletions auth/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ func (s *AuthenticationSuite) TestQueryToken() {
s.assertQueryRequest("token", "clienttoken_admin", s.auth.RequireAdmin, 200)
}

func (s *AuthenticationSuite) assertQueryRequest(key, value string, f fMiddleware, code int) {
func (s *AuthenticationSuite) assertQueryRequest(key, value string, f fMiddleware, code int) (ctx *gin.Context) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx, _ = gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/?%s=%s", key, value), nil)
f()(ctx)
assert.Equal(s.T(), code, recorder.Code)
return
}

func (s *AuthenticationSuite) TestNothingProvided() {
Expand Down Expand Up @@ -160,13 +161,42 @@ func (s *AuthenticationSuite) TestBasicAuth() {
s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.RequireAdmin, 401)
}

func (s *AuthenticationSuite) assertHeaderRequest(key, value string, f fMiddleware, code int) {
func (s *AuthenticationSuite) TestOptionalAuth() {
// various invalid users
ctx := s.assertQueryRequest("token", "ergerogerg", s.auth.Optional, 200)
assert.Nil(s.T(), TryGetUserID(ctx))
ctx = s.assertHeaderRequest("X-Gotify-Key", "ergerogerg", s.auth.Optional, 200)
assert.Nil(s.T(), TryGetUserID(ctx))
ctx = s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.Optional, 200)
assert.Nil(s.T(), TryGetUserID(ctx))
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHd4", s.auth.Optional, 200)
assert.Nil(s.T(), TryGetUserID(ctx))
ctx = s.assertQueryRequest("tokenx", "clienttoken", s.auth.Optional, 200)
assert.Nil(s.T(), TryGetUserID(ctx))
ctx = s.assertQueryRequest("token", "apptoken_admin", s.auth.Optional, 200)
assert.Nil(s.T(), TryGetUserID(ctx))

// user existing:pw
ctx = s.assertHeaderRequest("Authorization", "Basic ZXhpc3Rpbmc6cHc=", s.auth.Optional, 200)
assert.Equal(s.T(), uint(1), *TryGetUserID(ctx))
ctx = s.assertQueryRequest("token", "clienttoken", s.auth.Optional, 200)
assert.Equal(s.T(), uint(1), *TryGetUserID(ctx))

// user admin:pw
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.Optional, 200)
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
ctx = s.assertQueryRequest("token", "clienttoken_admin", s.auth.Optional, 200)
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
}

func (s *AuthenticationSuite) assertHeaderRequest(key, value string, f fMiddleware, code int) (ctx *gin.Context) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx, _ = gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest("GET", "/", nil)
ctx.Request.Header.Set(key, value)
f()(ctx)
assert.Equal(s.T(), code, recorder.Code)
return
}

type fMiddleware func() gin.HandlerFunc
15 changes: 12 additions & 3 deletions auth/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ func RegisterAuthentication(ctx *gin.Context, user *model.User, userID uint, tok

// GetUserID returns the user id which was previously registered by RegisterAuthentication.
func GetUserID(ctx *gin.Context) uint {
id := TryGetUserID(ctx)
if id == nil {
panic("token and user may not be null")
}
return *id
}

// TryGetUserID returns the user id or nil if one is not set.
func TryGetUserID(ctx *gin.Context) *uint {
user := ctx.MustGet("user").(*model.User)
if user == nil {
userID := ctx.MustGet("userid").(uint)
if userID == 0 {
panic("token and user may not be null")
return nil
}
return userID
return &userID
}

return user.ID
return &user.ID
}

// GetTokenID returns the tokenID.
Expand Down
8 changes: 8 additions & 0 deletions auth/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (s *UtilSuite) Test_getID() {
assert.Panics(s.T(), func() {
s.expectUserIDWith(nil, 0, 0)
})
s.expectTryUserIDWith(nil, 0, nil)
}

func (s *UtilSuite) Test_getToken() {
Expand All @@ -44,3 +45,10 @@ func (s *UtilSuite) expectUserIDWith(user *model.User, tokenUserID, expectedID u
actualID := GetUserID(ctx)
assert.Equal(s.T(), expectedID, actualID)
}

func (s *UtilSuite) expectTryUserIDWith(user *model.User, tokenUserID uint, expectedID *uint) {
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
RegisterAuthentication(ctx, user, tokenUserID, "")
actualID := TryGetUserID(ctx)
assert.Equal(s.T(), expectedID, actualID)
}
1 change: 1 addition & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ defaultuser: # on database creation, gotify creates an admin user
passstrength: 10 # the bcrypt password strength (higher = better but also slower)
uploadedimagesdir: data/images # the directory for storing uploaded images
pluginsdir: data/plugins # the directory where plugin resides
registration: false # enable registrations
Loading

0 comments on commit db864e0

Please sign in to comment.