Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
//
// Delete an application.
//
// Requires elevated authentication.
//
// ---
// consumes: [application/json]
// produces: [application/json]
Expand Down
62 changes: 62 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package api

import (
"errors"
"fmt"
"time"

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

// The ClientAPI provides handlers for managing clients and applications.
Expand Down Expand Up @@ -190,6 +193,8 @@ func (a *ClientAPI) GetClients(ctx *gin.Context) {
//
// Delete a client.
//
// Requires elevated authentication.
//
// ---
// consumes: [application/json]
// produces: [application/json]
Expand Down Expand Up @@ -235,6 +240,63 @@ func (a *ClientAPI) DeleteClient(ctx *gin.Context) {
})
}

// swagger:operation POST /client:elevate client elevateClient
//
// Elevate a client session.
//
// Requires elevated authentication.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// parameters:
// - name: body
// in: body
// description: the elevation request
// required: true
// schema:
// $ref: "#/definitions/ElevateRequest"
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// responses:
// 204:
// description: Ok
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 404:
// description: Not Found
// schema:
// $ref: "#/definitions/Error"
func (a *ClientAPI) ElevateClient(ctx *gin.Context) {
var params model.ElevateRequest
if err := ctx.Bind(&params); err != nil {
return
}

client, err := a.DB.GetClientByID(params.ID)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if client == nil || client.UserID != auth.GetUserID(ctx) {
ctx.AbortWithError(404, errors.New("client not found"))
return
}

elevatedUntil := time.Now().Add(time.Duration(params.DurationSeconds) * time.Second)
if err := a.DB.UpdateClientElevatedUntil(client.ID, &elevatedUntil); err != nil {
ctx.AbortWithError(500, err)
return
}

ctx.Status(204)
}

func (a *ClientAPI) clientExists(token string) bool {
client, _ := a.DB.GetClientByToken(token)
return client != nil
Expand Down
74 changes: 74 additions & 0 deletions api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/url"
"strings"
"testing"
"time"

"github.com/gin-gonic/gin"
"github.com/gotify/server/v2/mode"
Expand Down Expand Up @@ -223,11 +224,84 @@ func (s *ClientSuite) Test_UpdateClient_WithMissingAttributes_expectBadRequest()
assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *ClientSuite) Test_ElevateClient_expectSuccess() {
s.db.User(5).Client(8)

test.WithUser(s.ctx, 5)
s.withJSONBody(`{"id":8,"durationSeconds":900}`)

before := time.Now()
s.a.ElevateClient(s.ctx)
after := time.Now()

assert.Equal(s.T(), 204, s.ctx.Writer.Status())
client, err := s.db.GetClientByID(8)
assert.NoError(s.T(), err)
assert.NotNil(s.T(), client.ElevatedUntil)
assert.WithinRange(s.T(), *client.ElevatedUntil, before.Add(15*time.Minute), after.Add(15*time.Minute))
}

func (s *ClientSuite) Test_ElevateClient_expectNotFoundOnMissingClient() {
s.db.User(5)

test.WithUser(s.ctx, 5)
s.withJSONBody(`{"id":8,"durationSeconds":900}`)

s.a.ElevateClient(s.ctx)

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

func (s *ClientSuite) Test_ElevateClient_expectNotFoundOnCurrentUserIsNotOwner() {
s.db.User(5).Client(8)
s.db.User(2)

test.WithUser(s.ctx, 2)
s.withJSONBody(`{"id":8,"durationSeconds":900}`)

s.a.ElevateClient(s.ctx)

assert.Equal(s.T(), 404, s.recorder.Code)
client, err := s.db.GetClientByID(8)
assert.NoError(s.T(), err)
assert.Nil(s.T(), client.ElevatedUntil)
}

func (s *ClientSuite) Test_ElevateClient_expectBadRequestOnMissingID() {
s.db.User(5)

test.WithUser(s.ctx, 5)
s.withJSONBody(`{"durationSeconds":900}`)

s.a.ElevateClient(s.ctx)

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

func (s *ClientSuite) Test_ElevateClient_expectBadRequestOnMissingDuration() {
s.db.User(5).Client(8)

test.WithUser(s.ctx, 5)
s.withJSONBody(`{"id":8}`)

s.a.ElevateClient(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
client, err := s.db.GetClientByID(8)
assert.NoError(s.T(), err)
assert.Nil(s.T(), client.ElevatedUntil)
}

func (s *ClientSuite) withFormData(formData string) {
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(formData))
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}

func (s *ClientSuite) withJSONBody(body string) {
s.ctx.Request = httptest.NewRequest("POST", "/client:elevate", strings.NewReader(body))
s.ctx.Request.Header.Set("Content-Type", "application/json")
}

func withURL(ctx *gin.Context, scheme, host string) {
ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
}
6 changes: 1 addition & 5 deletions api/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ type MessageDatabase interface {
DeleteMessagesByUser(userID uint) error
DeleteMessagesByApplication(applicationID uint) error
CreateMessage(message *model.Message) error
GetApplicationByToken(token string) (*model.Application, error)
}

var timeNow = time.Now
Expand Down Expand Up @@ -364,10 +363,7 @@ func (a *MessageAPI) DeleteMessage(ctx *gin.Context) {
func (a *MessageAPI) CreateMessage(ctx *gin.Context) {
message := model.MessageExternal{}
if err := ctx.Bind(&message); err == nil {
application, err := a.DB.GetApplicationByToken(auth.GetTokenID(ctx))
if success := successOrAbort(ctx, 500, err); !success {
return
}
application := auth.GetApplication(ctx)
message.ApplicationID = application.ID
if strings.TrimSpace(message.Title) == "" {
message.Title = application.Name
Expand Down
33 changes: 11 additions & 22 deletions api/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,7 @@ func (s *MessageSuite) Test_CreateMessage_onJson_allParams() {
timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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