Skip to content

Commit 9e5a6b5

Browse files
authored
Merge pull request #952 from gotify/oidc-next
Client elevation
2 parents 6e588f3 + ef36e75 commit 9e5a6b5

45 files changed

Lines changed: 1457 additions & 259 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/application.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
151151
//
152152
// Delete an application.
153153
//
154+
// Requires elevated authentication.
155+
//
154156
// ---
155157
// consumes: [application/json]
156158
// produces: [application/json]

api/client.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package api
22

33
import (
4+
"errors"
45
"fmt"
6+
"time"
57

68
"github.com/gin-gonic/gin"
79
"github.com/gotify/server/v2/auth"
@@ -16,6 +18,7 @@ type ClientDatabase interface {
1618
GetClientsByUser(userID uint) ([]*model.Client, error)
1719
DeleteClientByID(id uint) error
1820
UpdateClient(client *model.Client) error
21+
UpdateClientElevatedUntil(id uint, t *time.Time) error
1922
}
2023

2124
// The ClientAPI provides handlers for managing clients and applications.
@@ -182,6 +185,12 @@ func (a *ClientAPI) GetClients(ctx *gin.Context) {
182185
if success := successOrAbort(ctx, 500, err); !success {
183186
return
184187
}
188+
now := time.Now()
189+
for _, client := range clients {
190+
if client.ElevatedUntil != nil && !now.Before(*client.ElevatedUntil) {
191+
client.ElevatedUntil = nil
192+
}
193+
}
185194
ctx.JSON(200, clients)
186195
}
187196

@@ -190,6 +199,8 @@ func (a *ClientAPI) GetClients(ctx *gin.Context) {
190199
//
191200
// Delete a client.
192201
//
202+
// Requires elevated authentication.
203+
//
193204
// ---
194205
// consumes: [application/json]
195206
// produces: [application/json]
@@ -235,6 +246,71 @@ func (a *ClientAPI) DeleteClient(ctx *gin.Context) {
235246
})
236247
}
237248

249+
// swagger:operation POST /client/{id}/elevate client elevateClient
250+
//
251+
// Elevate a client session.
252+
//
253+
// Requires elevated authentication.
254+
//
255+
// ---
256+
// consumes: [application/json]
257+
// produces: [application/json]
258+
// parameters:
259+
// - name: id
260+
// in: path
261+
// description: the client id
262+
// required: true
263+
// type: integer
264+
// format: int64
265+
// - name: body
266+
// in: body
267+
// description: the elevation request
268+
// required: true
269+
// schema:
270+
// $ref: "#/definitions/ElevateRequest"
271+
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
272+
// responses:
273+
// 204:
274+
// description: Ok
275+
// 400:
276+
// description: Bad Request
277+
// schema:
278+
// $ref: "#/definitions/Error"
279+
// 401:
280+
// description: Unauthorized
281+
// schema:
282+
// $ref: "#/definitions/Error"
283+
// 404:
284+
// description: Not Found
285+
// schema:
286+
// $ref: "#/definitions/Error"
287+
func (a *ClientAPI) ElevateClient(ctx *gin.Context) {
288+
withID(ctx, "id", func(id uint) {
289+
var params model.ElevateRequest
290+
if err := ctx.Bind(&params); err != nil {
291+
return
292+
}
293+
294+
client, err := a.DB.GetClientByID(id)
295+
if err != nil {
296+
ctx.AbortWithError(500, err)
297+
return
298+
}
299+
if client == nil || client.UserID != auth.GetUserID(ctx) {
300+
ctx.AbortWithError(404, errors.New("client not found"))
301+
return
302+
}
303+
304+
elevatedUntil := time.Now().Add(time.Duration(params.DurationSeconds) * time.Second)
305+
if err := a.DB.UpdateClientElevatedUntil(client.ID, &elevatedUntil); err != nil {
306+
ctx.AbortWithError(500, err)
307+
return
308+
}
309+
310+
ctx.Status(204)
311+
})
312+
}
313+
238314
func (a *ClientAPI) clientExists(token string) bool {
239315
client, _ := a.DB.GetClientByToken(token)
240316
return client != nil

api/client_test.go

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package api
22

33
import (
4+
"fmt"
45
"net/http/httptest"
56
"net/url"
67
"strings"
78
"testing"
9+
"time"
810

911
"github.com/gin-gonic/gin"
1012
"github.com/gotify/server/v2/mode"
@@ -164,7 +166,7 @@ func (s *ClientSuite) Test_DeleteClient_expectNotFound() {
164166

165167
test.WithUser(s.ctx, 5)
166168
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstClientToken, nil)
167-
s.ctx.Params = gin.Params{{Key: "id", Value: "8"}}
169+
s.ctx.AddParam("id", "8")
168170

169171
s.a.DeleteClient(s.ctx)
170172

@@ -176,7 +178,7 @@ func (s *ClientSuite) Test_DeleteClient() {
176178

177179
test.WithUser(s.ctx, 5)
178180
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstClientToken, nil)
179-
s.ctx.Params = gin.Params{{Key: "id", Value: "8"}}
181+
s.ctx.AddParam("id", "8")
180182

181183
assert.False(s.T(), s.notified)
182184

@@ -223,11 +225,90 @@ func (s *ClientSuite) Test_UpdateClient_WithMissingAttributes_expectBadRequest()
223225
assert.Equal(s.T(), 400, s.recorder.Code)
224226
}
225227

228+
func (s *ClientSuite) Test_ElevateClient_expectSuccess() {
229+
s.db.User(5).Client(8)
230+
231+
test.WithUser(s.ctx, 5)
232+
s.withElevateRequest(8, 900)
233+
234+
before := time.Now()
235+
s.a.ElevateClient(s.ctx)
236+
after := time.Now()
237+
238+
assert.Equal(s.T(), 204, s.ctx.Writer.Status())
239+
client, err := s.db.GetClientByID(8)
240+
assert.NoError(s.T(), err)
241+
assert.NotNil(s.T(), client.ElevatedUntil)
242+
assert.WithinRange(s.T(), *client.ElevatedUntil, before.Add(15*time.Minute), after.Add(15*time.Minute))
243+
}
244+
245+
func (s *ClientSuite) Test_ElevateClient_expectNotFoundOnMissingClient() {
246+
s.db.User(5)
247+
248+
test.WithUser(s.ctx, 5)
249+
s.withElevateRequest(8, 900)
250+
251+
s.a.ElevateClient(s.ctx)
252+
253+
assert.Equal(s.T(), 404, s.recorder.Code)
254+
}
255+
256+
func (s *ClientSuite) Test_ElevateClient_expectNotFoundOnCurrentUserIsNotOwner() {
257+
s.db.User(5).Client(8)
258+
s.db.User(2)
259+
260+
test.WithUser(s.ctx, 2)
261+
s.withElevateRequest(8, 900)
262+
263+
s.a.ElevateClient(s.ctx)
264+
265+
assert.Equal(s.T(), 404, s.recorder.Code)
266+
client, err := s.db.GetClientByID(8)
267+
assert.NoError(s.T(), err)
268+
assert.Nil(s.T(), client.ElevatedUntil)
269+
}
270+
271+
func (s *ClientSuite) Test_ElevateClient_expectBadRequestOnMissingID() {
272+
s.db.User(5)
273+
274+
test.WithUser(s.ctx, 5)
275+
s.withElevateBody(`{"durationSeconds":900}`)
276+
277+
s.a.ElevateClient(s.ctx)
278+
279+
assert.Equal(s.T(), 400, s.recorder.Code)
280+
}
281+
282+
func (s *ClientSuite) Test_ElevateClient_expectBadRequestOnMissingDuration() {
283+
s.db.User(5).Client(8)
284+
285+
test.WithUser(s.ctx, 5)
286+
s.ctx.AddParam("id", "8")
287+
s.withElevateBody(`{}`)
288+
289+
s.a.ElevateClient(s.ctx)
290+
291+
assert.Equal(s.T(), 400, s.recorder.Code)
292+
client, err := s.db.GetClientByID(8)
293+
assert.NoError(s.T(), err)
294+
assert.Nil(s.T(), client.ElevatedUntil)
295+
}
296+
226297
func (s *ClientSuite) withFormData(formData string) {
227298
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(formData))
228299
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
229300
}
230301

302+
func (s *ClientSuite) withElevateRequest(id uint, durationSeconds int) {
303+
s.ctx.AddParam("id", fmt.Sprintf("%d", id))
304+
s.withElevateBody(fmt.Sprintf(`{"durationSeconds":%d}`, durationSeconds))
305+
}
306+
307+
func (s *ClientSuite) withElevateBody(body string) {
308+
s.ctx.Request = httptest.NewRequest("POST", "/client/ignored/elevate", strings.NewReader(body))
309+
s.ctx.Request.Header.Set("Content-Type", "application/json")
310+
}
311+
231312
func withURL(ctx *gin.Context, scheme, host string) {
232313
ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
233314
}

api/message.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ type MessageDatabase interface {
2424
DeleteMessagesByUser(userID uint) error
2525
DeleteMessagesByApplication(applicationID uint) error
2626
CreateMessage(message *model.Message) error
27-
GetApplicationByToken(token string) (*model.Application, error)
2827
}
2928

3029
var timeNow = time.Now
@@ -364,10 +363,7 @@ func (a *MessageAPI) DeleteMessage(ctx *gin.Context) {
364363
func (a *MessageAPI) CreateMessage(ctx *gin.Context) {
365364
message := model.MessageExternal{}
366365
if err := ctx.Bind(&message); err == nil {
367-
application, err := a.DB.GetApplicationByToken(auth.GetTokenID(ctx))
368-
if success := successOrAbort(ctx, 500, err); !success {
369-
return
370-
}
366+
application := auth.GetApplication(ctx)
371367
message.ApplicationID = application.ID
372368
if strings.TrimSpace(message.Title) == "" {
373369
message.Title = application.Name

api/message_test.go

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,7 @@ func (s *MessageSuite) Test_CreateMessage_onJson_allParams() {
322322
timeNow = func() time.Time { return t }
323323
defer func() { timeNow = time.Now }()
324324

325-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
326-
s.db.User(4).AppWithToken(7, "app-token")
325+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithToken(7, "app-token"))
327326
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": 1}`))
328327
s.ctx.Request.Header.Set("Content-Type", "application/json")
329328

@@ -344,8 +343,7 @@ func (s *MessageSuite) Test_CreateMessage_WithDefaultPriority() {
344343
timeNow = func() time.Time { return t }
345344
defer func() { timeNow = time.Now }()
346345

347-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
348-
s.db.User(4).AppWithTokenAndDefaultPriority(8, "app-token", 5)
346+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithTokenAndDefaultPriority(8, "app-token", 5))
349347
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`))
350348
s.ctx.Request.Header.Set("Content-Type", "application/json")
351349

@@ -365,8 +363,7 @@ func (s *MessageSuite) Test_CreateMessage_WithTitle() {
365363
timeNow = func() time.Time { return t }
366364
defer func() { timeNow = time.Now }()
367365

368-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
369-
s.db.User(4).AppWithToken(5, "app-token")
366+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithToken(5, "app-token"))
370367
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`))
371368
s.ctx.Request.Header.Set("Content-Type", "application/json")
372369

@@ -382,8 +379,7 @@ func (s *MessageSuite) Test_CreateMessage_WithTitle() {
382379
}
383380

384381
func (s *MessageSuite) Test_CreateMessage_failWhenNoMessage() {
385-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
386-
s.db.User(4).AppWithToken(1, "app-token")
382+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithToken(1, "app-token"))
387383

388384
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle"}`))
389385
s.ctx.Request.Header.Set("Content-Type", "application/json")
@@ -398,8 +394,7 @@ func (s *MessageSuite) Test_CreateMessage_failWhenNoMessage() {
398394
}
399395

400396
func (s *MessageSuite) Test_CreateMessage_WithoutTitle() {
401-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
402-
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
397+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithTokenAndName(8, "app-token", "Application name"))
403398

404399
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage"}`))
405400
s.ctx.Request.Header.Set("Content-Type", "application/json")
@@ -415,8 +410,7 @@ func (s *MessageSuite) Test_CreateMessage_WithoutTitle() {
415410
}
416411

417412
func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() {
418-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
419-
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
413+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithTokenAndName(8, "app-token", "Application name"))
420414

421415
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage", "title": " "}`))
422416
s.ctx.Request.Header.Set("Content-Type", "application/json")
@@ -432,8 +426,7 @@ func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() {
432426
}
433427

434428
func (s *MessageSuite) Test_CreateMessage_IgnoreID() {
435-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
436-
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
429+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithTokenAndName(8, "app-token", "Application name"))
437430

438431
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage", "id": 1337}`))
439432
s.ctx.Request.Header.Set("Content-Type", "application/json")
@@ -448,8 +441,7 @@ func (s *MessageSuite) Test_CreateMessage_IgnoreID() {
448441
}
449442

450443
func (s *MessageSuite) Test_CreateMessage_WithExtras() {
451-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
452-
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
444+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithTokenAndName(8, "app-token", "Application name"))
453445

454446
t, _ := time.Parse("2006/01/02", "2017/01/02")
455447
timeNow = func() time.Time { return t }
@@ -487,8 +479,7 @@ func (s *MessageSuite) Test_CreateMessage_WithExtras() {
487479
}
488480

489481
func (s *MessageSuite) Test_CreateMessage_failWhenPriorityNotNumber() {
490-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
491-
s.db.User(4).AppWithToken(8, "app-token")
482+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithToken(8, "app-token"))
492483

493484
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": "asd"}`))
494485
s.ctx.Request.Header.Set("Content-Type", "application/json")
@@ -503,8 +494,7 @@ func (s *MessageSuite) Test_CreateMessage_failWhenPriorityNotNumber() {
503494
}
504495

505496
func (s *MessageSuite) Test_CreateMessage_onQueryData() {
506-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
507-
s.db.User(4).AppWithToken(2, "app-token")
497+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithToken(2, "app-token"))
508498

509499
t, _ := time.Parse("2006/01/02", "2017/01/02")
510500
timeNow = func() time.Time { return t }
@@ -526,8 +516,7 @@ func (s *MessageSuite) Test_CreateMessage_onQueryData() {
526516
}
527517

528518
func (s *MessageSuite) Test_CreateMessage_onFormData() {
529-
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
530-
s.db.User(4).AppWithToken(99, "app-token")
519+
auth.RegisterApplication(s.ctx, s.db.User(4).NewAppWithToken(99, "app-token"))
531520

532521
t, _ := time.Parse("2006/01/02", "2017/01/02")
533522
timeNow = func() time.Time { return t }

0 commit comments

Comments
 (0)