Skip to content

Commit e6361ca

Browse files
authored
Merge pull request #1048 from gotify/fix-create-user-permission
Fix create user permission
2 parents 0c24eda + 00d89c0 commit e6361ca

8 files changed

Lines changed: 124 additions & 94 deletions

File tree

‎api/user.go‎

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
153153
// Create a user.
154154
//
155155
// With enabled registration: non admin users can be created without authentication.
156-
// With disabled registrations: users can only be created by admin users.
156+
// With disabled registrations: users can only be created by admin users with an elevated session.
157157
//
158158
// Requires elevated authentication.
159159
//
@@ -202,36 +202,25 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) {
202202
Admin: user.Admin,
203203
Pass: pw,
204204
}
205-
existingUser, err := a.DB.GetUserByName(internal.Name)
206-
if success := successOrAbort(ctx, 500, err); !success {
207-
return
208-
}
209-
210-
var requestedBy *model.User
211-
uid := auth.TryGetUserID(ctx)
212-
if uid != nil {
213-
requestedBy, err = a.DB.GetUserByID(*uid)
214-
if err != nil {
215-
ctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("could not get user: %s", err))
216-
return
217-
}
218-
}
219205

220-
if requestedBy == nil || !requestedBy.Admin {
221-
status := http.StatusUnauthorized
222-
if requestedBy != nil {
223-
status = http.StatusForbidden
224-
}
206+
// The auth middleware guarantees authenticated requests to be elevated admins.
207+
// Only unauthenticated requests are limited to the registration checks.
208+
if auth.TryGetUserID(ctx) == nil {
225209
if !a.Registration {
226-
ctx.AbortWithError(status, errors.New("you are not allowed to access this api"))
210+
ctx.AbortWithError(http.StatusUnauthorized, errors.New("you are not allowed to access this api"))
227211
return
228212
}
229213
if internal.Admin {
230-
ctx.AbortWithError(status, errors.New("you are not allowed to create an admin user"))
214+
ctx.AbortWithError(http.StatusUnauthorized, errors.New("you are not allowed to create an admin user"))
231215
return
232216
}
233217
}
234218

219+
existingUser, err := a.DB.GetUserByName(internal.Name)
220+
if success := successOrAbort(ctx, 500, err); !success {
221+
return
222+
}
223+
235224
if existingUser == nil {
236225
if success := successOrAbort(ctx, 500, a.DB.CreateUser(internal)); !success {
237226
return

‎api/user_test.go‎

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -187,45 +187,6 @@ func (s *UserSuite) Test_CreateUser() {
187187
assert.True(s.T(), s.notifiedAdd)
188188
}
189189

190-
func (s *UserSuite) Test_CreateUser_ByNonAdmin() {
191-
s.loginUser()
192-
193-
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
194-
s.ctx.Request.Header.Set("Content-Type", "application/json")
195-
196-
s.a.CreateUser(s.ctx)
197-
198-
assert.Equal(s.T(), 403, s.recorder.Code)
199-
}
200-
201-
func (s *UserSuite) Test_CreateUser_Register_ByNonAdmin() {
202-
s.loginUser()
203-
s.a.Registration = true
204-
205-
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
206-
s.ctx.Request.Header.Set("Content-Type", "application/json")
207-
208-
s.a.CreateUser(s.ctx)
209-
210-
assert.Equal(s.T(), 200, s.recorder.Code)
211-
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
212-
assert.NotNil(s.T(), created)
213-
}
214-
}
215-
216-
func (s *UserSuite) Test_CreateUser_Register_Admin_ByNonAdmin() {
217-
s.a.Registration = true
218-
s.loginUser()
219-
220-
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": true}`))
221-
s.ctx.Request.Header.Set("Content-Type", "application/json")
222-
223-
s.a.CreateUser(s.ctx)
224-
225-
assert.Equal(s.T(), 403, s.recorder.Code)
226-
s.db.AssertUsernameNotExist("tom")
227-
}
228-
229190
func (s *UserSuite) Test_CreateUser_Anonymous() {
230191
s.noLogin()
231192

@@ -510,11 +471,6 @@ func (s *UserSuite) loginAdmin() {
510471
auth.RegisterUser(s.ctx, &model.User{ID: 1, Admin: true})
511472
}
512473

513-
func (s *UserSuite) loginUser() {
514-
s.db.CreateUser(&model.User{ID: 1, Name: "user", Admin: false})
515-
auth.RegisterUser(s.ctx, &model.User{ID: 1})
516-
}
517-
518474
func (s *UserSuite) noLogin() {
519475
// No authentication is registered
520476
}

‎auth/authentication.go‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/gin-gonic/gin"
1010
"github.com/gotify/server/v3/auth/password"
1111
"github.com/gotify/server/v3/model"
12+
"github.com/rs/zerolog/log"
1213
)
1314

1415
type authState int
@@ -48,7 +49,22 @@ type Auth struct {
4849

4950
// RequireAdmin requires an elevated client token or basic auth, the user must be an admin.
5051
func (a *Auth) RequireAdmin(ctx *gin.Context) {
51-
a.evaluateOr401(ctx, a.handleUser(a.checkUserAdmin), a.handleClient(a.checkClientAdmin, a.checkClientElevated))
52+
a.evaluateOr401(ctx, a.adminHandlers()...)
53+
}
54+
55+
// OptionalAdmin allows optional authentication. When authentication is present
56+
// an elevated client token or basic auth of an admin user must be provided.
57+
func (a *Auth) OptionalAdmin(ctx *gin.Context) {
58+
if !a.evaluate(ctx, a.adminHandlers()...) {
59+
ctx.Next()
60+
}
61+
}
62+
63+
func (a *Auth) adminHandlers() []func(ctx *gin.Context) (authState, error) {
64+
return []func(ctx *gin.Context) (authState, error){
65+
a.handleUser(a.checkUserAdmin),
66+
a.handleClient(a.checkClientAdmin, a.checkClientElevated),
67+
}
5268
}
5369

5470
// RequireClient returns a gin middleware which requires a client token or basic authentication header to be supplied
@@ -84,12 +100,6 @@ func (a *Auth) RequireApplicationOrClient(ctx *gin.Context) {
84100
a.evaluateOr401(ctx, a.handleApplication, a.handleClient(), a.handleUser())
85101
}
86102

87-
func (a *Auth) Optional(ctx *gin.Context) {
88-
if !a.evaluate(ctx, a.handleUser(), a.handleClient()) {
89-
ctx.Next()
90-
}
91-
}
92-
93103
func (a *Auth) evaluate(ctx *gin.Context, funcs ...func(ctx *gin.Context) (authState, error)) bool {
94104
if a.rejectForeignOrigin(ctx) {
95105
return true
@@ -298,6 +308,9 @@ func (a *Auth) tokenFromAuthorizationHeader(ctx *gin.Context) string {
298308
func (a *Auth) checkClientAdmin(client *model.Client) (authState, error) {
299309
if user, err := a.DB.GetUserByID(client.UserID); err != nil {
300310
return authStateSkip, err
311+
} else if user == nil {
312+
log.Warn().Uint("client_id", client.ID).Uint("user_id", client.UserID).Msg("User for authenticated client doesn't exist")
313+
return authStateForbidden, nil
301314
} else if !user.Admin {
302315
return authStateForbidden, nil
303316
}

‎auth/authentication_test.go‎

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func (s *AuthenticationSuite) SetupSuite() {
6363
{Token: "clienttoken_admin_elevated", Name: "elevated phone2", ElevatedUntil: &elevated},
6464
},
6565
})
66+
67+
// client whose user does not exist anymore
68+
s.DB.CreateClient(&model.Client{Token: "clienttoken_orphan_elevated", Name: "orphan phone", UserID: 999, ElevatedUntil: &elevated})
6669
}
6770

6871
func (s *AuthenticationSuite) TearDownSuite() {
@@ -141,6 +144,11 @@ func (s *AuthenticationSuite) TestNothingProvided() {
141144
assert.Equal(s.T(), 401, recorder.Code)
142145
}
143146

147+
func (s *AuthenticationSuite) TestOrphanedClientToken() {
148+
s.assertHeaderRequest("X-Gotify-Key", "clienttoken_orphan_elevated", s.auth.RequireAdmin, 403)
149+
s.assertHeaderRequest("X-Gotify-Key", "clienttoken_orphan_elevated", s.auth.OptionalAdmin, 403)
150+
}
151+
144152
func (s *AuthenticationSuite) TestHeaderApiKeyToken() {
145153
// not existing token
146154
s.assertHeaderRequest("X-Gotify-Key", "ergerogerg", s.auth.RequireApplicationToken, 401)
@@ -282,33 +290,47 @@ func (s *AuthenticationSuite) TestBasicAuthDisabled() {
282290
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.RequireClient, 403)
283291
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.RequireAdmin, 403)
284292
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.RequireElevatedClient, 403)
293+
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.OptionalAdmin, 403)
285294
}
286295

287-
func (s *AuthenticationSuite) TestOptionalAuth() {
288-
// various invalid users
289-
ctx := s.assertQueryRequest("token", "ergerogerg", s.auth.Optional, 200)
290-
assert.Nil(s.T(), TryGetUserID(ctx))
291-
ctx = s.assertHeaderRequest("X-Gotify-Key", "ergerogerg", s.auth.Optional, 200)
296+
func (s *AuthenticationSuite) TestOptionalAdminAuth() {
297+
ctx := s.assertQueryRequest("token", "ergerogerg", s.auth.OptionalAdmin, 200)
292298
assert.Nil(s.T(), TryGetUserID(ctx))
293-
ctx = s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.Optional, 200)
299+
ctx = s.assertQueryRequest("token", "apptoken_admin", s.auth.OptionalAdmin, 200)
294300
assert.Nil(s.T(), TryGetUserID(ctx))
295-
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHd4", s.auth.Optional, 200)
296-
assert.Nil(s.T(), TryGetUserID(ctx))
297-
ctx = s.assertQueryRequest("tokenx", "clienttoken", s.auth.Optional, 200)
298-
assert.Nil(s.T(), TryGetUserID(ctx))
299-
ctx = s.assertQueryRequest("token", "apptoken_admin", s.auth.Optional, 200)
301+
302+
// nothing provided
303+
ctx = s.assertRequest(s.auth.OptionalAdmin, 200)
300304
assert.Nil(s.T(), TryGetUserID(ctx))
301305

302-
// user existing:pw
303-
ctx = s.assertHeaderRequest("Authorization", "Basic ZXhpc3Rpbmc6cHc=", s.auth.Optional, 200)
304-
assert.Equal(s.T(), uint(1), *TryGetUserID(ctx))
305-
ctx = s.assertQueryRequest("token", "clienttoken", s.auth.Optional, 200)
306-
assert.Equal(s.T(), uint(1), *TryGetUserID(ctx))
306+
// user existing:pw (non-admin)
307+
s.assertHeaderRequest("Authorization", "Basic ZXhpc3Rpbmc6cHc=", s.auth.OptionalAdmin, 403)
307308

308-
// user admin:pw
309-
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.Optional, 200)
309+
// clienttoken (non-admin, not elevated)
310+
s.assertQueryRequest("token", "clienttoken", s.auth.OptionalAdmin, 403)
311+
s.assertHeaderRequest("X-Gotify-Key", "clienttoken", s.auth.OptionalAdmin, 403)
312+
s.assertHeaderRequest("Authorization", "Bearer clienttoken", s.auth.OptionalAdmin, 403)
313+
314+
// clienttoken_elevated (non-admin, elevated)
315+
s.assertQueryRequest("token", "clienttoken_elevated", s.auth.OptionalAdmin, 403)
316+
s.assertHeaderRequest("X-Gotify-Key", "clienttoken_elevated", s.auth.OptionalAdmin, 403)
317+
s.assertHeaderRequest("Authorization", "Bearer clienttoken_elevated", s.auth.OptionalAdmin, 403)
318+
319+
// user admin:pw (basic auth counts as elevated)
320+
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.OptionalAdmin, 200)
310321
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
311-
ctx = s.assertQueryRequest("token", "clienttoken_admin", s.auth.Optional, 200)
322+
323+
// clienttoken_admin (not elevated)
324+
s.assertQueryRequest("token", "clienttoken_admin", s.auth.OptionalAdmin, 403)
325+
s.assertHeaderRequest("X-Gotify-Key", "clienttoken_admin", s.auth.OptionalAdmin, 403)
326+
s.assertHeaderRequest("Authorization", "Bearer clienttoken_admin", s.auth.OptionalAdmin, 403)
327+
328+
// clienttoken_admin_elevated
329+
ctx = s.assertQueryRequest("token", "clienttoken_admin_elevated", s.auth.OptionalAdmin, 200)
330+
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
331+
ctx = s.assertHeaderRequest("X-Gotify-Key", "clienttoken_admin_elevated", s.auth.OptionalAdmin, 200)
332+
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
333+
ctx = s.assertHeaderRequest("Authorization", "Bearer clienttoken_admin_elevated", s.auth.OptionalAdmin, 200)
312334
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
313335
}
314336

@@ -368,6 +390,15 @@ func (s *AuthenticationSuite) assertHeaderRequest(key, value string, f fMiddlewa
368390
return ctx
369391
}
370392

393+
func (s *AuthenticationSuite) assertRequest(f fMiddleware, code int) (ctx *gin.Context) {
394+
recorder := httptest.NewRecorder()
395+
ctx, _ = gin.CreateTestContext(recorder)
396+
ctx.Request = httptest.NewRequest("GET", "/", nil)
397+
f(ctx)
398+
assert.Equal(s.T(), code, recorder.Code)
399+
return ctx
400+
}
401+
371402
func (s *AuthenticationSuite) TestCookieCrossOriginProtection() {
372403
// httptest sets the request host to example.com.
373404
s.assertCsrfRequest(map[string]string{"Origin": "http://example.com"}, "clienttoken", s.auth.RequireClient, 200)

‎docs/spec.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2245,7 +2245,7 @@
22452245
"basicAuth": []
22462246
}
22472247
],
2248-
"description": "With enabled registration: non admin users can be created without authentication.\nWith disabled registrations: users can only be created by admin users.\n\nRequires elevated authentication.",
2248+
"description": "With enabled registration: non admin users can be created without authentication.\nWith disabled registrations: users can only be created by admin users with an elevated session.\n\nRequires elevated authentication.",
22492249
"consumes": [
22502250
"application/json"
22512251
],

‎router/router.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
158158
}
159159
}
160160

161-
g.Group("/user").Use(authentication.Optional).POST("", userHandler.CreateUser)
161+
g.Group("/user").Use(authentication.OptionalAdmin).POST("", userHandler.CreateUser)
162162

163163
g.POST("/auth/local/login", sessionHandler.Login)
164164

‎router/router_test.go‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,33 @@ func (s *IntegrationSuite) TestAuthentication() {
407407
assert.Equal(s.T(), "android-client", token.Name)
408408
}
409409

410+
func (s *IntegrationSuite) TestCreateUser_RequiresElevatedAdmin() {
411+
s.db.AdminUser(2).ClientWithToken(1, "Cadminplain").ElevatedClientWithToken(2, "Cadminelevated")
412+
s.db.User(3).ElevatedClientWithToken(3, "Cnormalelevated")
413+
414+
body := `{"name": "newadmin", "pass": "secret", "admin": true}`
415+
416+
// admin, but not elevated
417+
req := s.newRequest("POST", "user", body)
418+
req.Header.Set("X-Gotify-Key", "Cadminplain")
419+
doRequestAndExpect(s.T(), req, 403, `{"error":"Forbidden", "errorCode":403, "errorDescription":"session not elevated, use basic auth or call /client:elevate"}`)
420+
s.db.AssertUsernameNotExist("newadmin")
421+
422+
// elevated, but not admin
423+
req = s.newRequest("POST", "user", body)
424+
req.Header.Set("X-Gotify-Key", "Cnormalelevated")
425+
doRequestAndExpect(s.T(), req, 403, forbiddenJSON)
426+
s.db.AssertUsernameNotExist("newadmin")
427+
428+
// elevated admin
429+
req = s.newRequest("POST", "user", body)
430+
req.Header.Set("X-Gotify-Key", "Cadminelevated")
431+
doRequestAndExpect(s.T(), req, 200, `{"id": 4, "name": "newadmin", "admin": true, "createdAt":"2020-01-01T00:00:00Z"}`)
432+
created, err := s.db.GetUserByName("newadmin")
433+
assert.NoError(s.T(), err)
434+
assert.NotNil(s.T(), created)
435+
}
436+
410437
func (s *IntegrationSuite) newRequest(method, url, body string) *http.Request {
411438
req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", s.server.URL, url), strings.NewReader(body))
412439
req.Header.Add("Content-Type", "application/json")

‎test/testdb/database.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ func (d *Database) User(id uint) *AppClientBuilder {
5454
return &AppClientBuilder{db: d, userID: id}
5555
}
5656

57+
// AdminUser creates an admin user and returns a builder for applications and clients.
58+
func (d *Database) AdminUser(id uint) *AppClientBuilder {
59+
d.CreateUser(&model.User{ID: id, Name: "user" + fmt.Sprint(id), Admin: true})
60+
return &AppClientBuilder{db: d, userID: id}
61+
}
62+
5763
// NewUser creates a user and returns the user.
5864
func (d *Database) NewUser(id uint) *model.User {
5965
return d.NewUserWithName(id, "user"+fmt.Sprint(id))
@@ -173,6 +179,14 @@ func (ab *AppClientBuilder) NewClientWithToken(id uint, token string) *model.Cli
173179
return client
174180
}
175181

182+
// ElevatedClientWithToken creates an elevated client.
183+
func (ab *AppClientBuilder) ElevatedClientWithToken(id uint, token string) *AppClientBuilder {
184+
future := time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC)
185+
client := &model.Client{ID: id, Token: token, UserID: ab.userID, ElevatedUntil: &future}
186+
ab.db.CreateClient(client)
187+
return ab
188+
}
189+
176190
// Message creates a message and returns itself.
177191
func (mb *MessageBuilder) Message(id uint) *MessageBuilder {
178192
mb.NewMessage(id)

0 commit comments

Comments
 (0)