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
57 changes: 39 additions & 18 deletions api/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,17 +325,20 @@ func (a *MessageAPI) DeleteMessage(ctx *gin.Context) {
})
}

// CreateMessage creates a message, authentication via application-token is required.
// CreateMessage creates a message, authentication via application token, client token, or basic auth is required.
// swagger:operation POST /message message createMessage
//
// Create a message.
//
// __NOTE__: This API ONLY accepts an application token as authentication.
// __NOTE__: When authenticating with a client token or basic auth, the request body
// must include "appid" referencing an application owned by the authenticated user.
// When authenticating with an application token, the application is derived from the
// token and any "appid" in the body is ignored.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [appTokenAuthorizationHeader: [], appTokenHeader: [], appTokenQuery: []]
// security: [appTokenAuthorizationHeader: [], appTokenHeader: [], appTokenQuery: [], clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: body
// in: body
Expand All @@ -362,26 +365,44 @@ func (a *MessageAPI) DeleteMessage(ctx *gin.Context) {
// $ref: "#/definitions/Error"
func (a *MessageAPI) CreateMessage(ctx *gin.Context) {
message := model.MessageExternal{}
if err := ctx.Bind(&message); err == nil {
application := auth.GetApplication(ctx)
message.ApplicationID = application.ID
if strings.TrimSpace(message.Title) == "" {
message.Title = application.Name
}
if err := ctx.Bind(&message); err != nil {
return
}

if message.Priority == nil {
message.Priority = &application.DefaultPriority
app := auth.GetApplication(ctx)
if app == nil {
if message.ApplicationID == 0 {
ctx.AbortWithError(400, errors.New("appid is required when not authenticating with an application token"))
return
}

message.Date = timeNow()
message.ID = 0
msgInternal := toInternalMessage(&message)
if success := successOrAbort(ctx, 500, a.DB.CreateMessage(msgInternal)); !success {
fetchedApp, err := a.DB.GetApplicationByID(message.ApplicationID)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if fetchedApp == nil || fetchedApp.UserID != auth.GetUserID(ctx) {
ctx.AbortWithError(400, errors.New("appid not found"))
return
}
a.Notifier.Notify(auth.GetUserID(ctx), toExternalMessage(msgInternal))
ctx.JSON(200, toExternalMessage(msgInternal))
app = fetchedApp
}

message.ApplicationID = app.ID
if strings.TrimSpace(message.Title) == "" {
message.Title = app.Name
}

if message.Priority == nil {
message.Priority = &app.DefaultPriority
}

message.Date = timeNow()
message.ID = 0
msgInternal := toInternalMessage(&message)
if success := successOrAbort(ctx, 500, a.DB.CreateMessage(msgInternal)); !success {
return
}
a.Notifier.Notify(auth.GetUserID(ctx), toExternalMessage(msgInternal))
ctx.JSON(200, toExternalMessage(msgInternal))
}

func toInternalMessage(msg *model.MessageExternal) *model.Message {
Expand Down
113 changes: 113 additions & 0 deletions api/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,119 @@ func (s *MessageSuite) Test_CreateMessage_onFormData() {
assert.Equal(s.T(), uint(1), s.notifiedMessage.ID)
}

func (s *MessageSuite) Test_CreateMessage_clientToken_usesBodyAppId() {
t, _ := time.Parse("2006/01/02", "2017/01/02")
timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }()

user := s.db.User(4)
user.NewAppWithToken(7, "app-token")
auth.RegisterClient(s.ctx, user.NewClientWithToken(1, "client-token"))

s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"appid": 7, "title": "mytitle", "message": "mymessage", "priority": 1}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateMessage(s.ctx)

msgs, err := s.db.GetMessagesByApplication(7)
assert.NoError(s.T(), err)
expected := &model.MessageExternal{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: intPtr(1), Date: t}
assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code)
assert.Equal(s.T(), expected, s.notifiedMessage)
}

func (s *MessageSuite) Test_CreateMessage_clientToken_missingAppId_400() {
user := s.db.User(4)
user.NewAppWithToken(7, "app-token")
auth.RegisterClient(s.ctx, user.NewClientWithToken(1, "client-token"))

s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateMessage(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
assert.Nil(s.T(), s.notifiedMessage)
if msgs, err := s.db.GetMessagesByApplication(7); assert.NoError(s.T(), err) {
assert.Empty(s.T(), msgs)
}
}

func (s *MessageSuite) Test_CreateMessage_clientToken_appNotOwned_400() {
s.db.User(5).NewAppWithToken(7, "other-app-token")
auth.RegisterClient(s.ctx, s.db.User(4).NewClientWithToken(1, "client-token"))

s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"appid": 7, "message": "mymessage"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateMessage(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
assert.Nil(s.T(), s.notifiedMessage)
if msgs, err := s.db.GetMessagesByApplication(7); assert.NoError(s.T(), err) {
assert.Empty(s.T(), msgs)
}
}

func (s *MessageSuite) Test_CreateMessage_clientToken_unknownAppId_400() {
auth.RegisterClient(s.ctx, s.db.User(4).NewClientWithToken(1, "client-token"))

s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"appid": 999, "message": "mymessage"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateMessage(s.ctx)

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

func (s *MessageSuite) Test_CreateMessage_basicAuth_usesBodyAppId() {
t, _ := time.Parse("2006/01/02", "2017/01/02")
timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }()

s.db.User(4).NewAppWithToken(7, "app-token")
test.WithUser(s.ctx, 4)

s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"appid": 7, "title": "mytitle", "message": "mymessage", "priority": 1}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateMessage(s.ctx)

msgs, err := s.db.GetMessagesByApplication(7)
assert.NoError(s.T(), err)
expected := &model.MessageExternal{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: intPtr(1), Date: t}
assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code)
assert.Equal(s.T(), expected, s.notifiedMessage)
}

func (s *MessageSuite) Test_CreateMessage_appToken_ignoresBodyAppId() {
t, _ := time.Parse("2006/01/02", "2017/01/02")
timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }()

user := s.db.User(4)
user.NewAppWithToken(7, "other-app-token")
auth.RegisterApplication(s.ctx, user.NewAppWithToken(8, "app-token"))

s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"appid": 7, "title": "mytitle", "message": "mymessage", "priority": 1}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.CreateMessage(s.ctx)

msgs, err := s.db.GetMessagesByApplication(8)
assert.NoError(s.T(), err)
assert.Len(s.T(), msgs, 1)
if msgs7, err := s.db.GetMessagesByApplication(7); assert.NoError(s.T(), err) {
assert.Empty(s.T(), msgs7)
}
assert.Equal(s.T(), 200, s.recorder.Code)
}

func (s *MessageSuite) withURL(scheme, host, path, query string) {
s.ctx.Request.URL = &url.URL{Path: path, RawQuery: query}
s.ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
Expand Down
5 changes: 5 additions & 0 deletions auth/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ func (a *Auth) RequireApplicationToken(ctx *gin.Context) {
a.abort401(ctx)
}

// RequireAny requires client, application, or basic auth.
func (a *Auth) RequireApplicationOrClient(ctx *gin.Context) {
a.evaluateOr401(ctx, a.handleApplication, a.handleClient(), a.handleUser())
}

func (a *Auth) Optional(ctx *gin.Context) {
if !a.evaluate(ctx, a.handleUser(), a.handleClient()) {
ctx.Next()
Expand Down
14 changes: 13 additions & 1 deletion docs/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1438,9 +1438,21 @@
},
{
"appTokenQuery": []
},
{
"clientTokenAuthorizationHeader": []
},
{
"clientTokenHeader": []
},
{
"clientTokenQuery": []
},
{
"basicAuth": []
}
],
"description": "__NOTE__: This API ONLY accepts an application token as authentication.",
"description": "__NOTE__: When authenticating with a client token or basic auth, the request body\nmust include \"appid\" referencing an application owned by the authenticated user.\nWhen authenticating with an application token, the application is derived from the\ntoken and any \"appid\" in the body is ignored.",
"consumes": [
"application/json"
],
Expand Down
2 changes: 1 addition & 1 deletion router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
ctx.JSON(200, &model.GotifyInfo{Version: vInfo.Version, Oidc: conf.OIDC.Enabled, Register: conf.Registration})
})

g.Group("/").Use(authentication.RequireApplicationToken).POST("/message", messageHandler.CreateMessage)
g.Group("/").Use(authentication.RequireApplicationOrClient).POST("/message", messageHandler.CreateMessage)

clientAuth := g.Group("")
{
Expand Down
2 changes: 1 addition & 1 deletion router/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ func (s *IntegrationSuite) TestAuthentication() {

req = s.newRequest("POST", "message", `{"message": "backup done", "title": "backup"}`)
req.SetBasicAuth("normal", "secret")
doRequestAndExpect(s.T(), req, 403, forbiddenJSON)
doRequestAndExpect(s.T(), req, 400, `{"error":"Bad Request", "errorCode":400, "errorDescription":"appid is required when not authenticating with an application token"}`)

req = s.newRequest("GET", "current/user", "")
req.SetBasicAuth("normal", "secret")
Expand Down
7 changes: 3 additions & 4 deletions ui/src/message/MessagesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,14 @@ export class MessagesStore {
priority: number
): Promise<void> => {
const app = this.appStore.getByID(appId);
const payload: Pick<IMessage, 'title' | 'message' | 'priority'> = {
const payload: Pick<IMessage, 'appid' | 'title' | 'message' | 'priority'> = {
appid: appId,
message,
priority,
title,
};

await axios.post(`${config.get('url')}message`, payload, {
headers: {'X-Gotify-Key': app.token},
});
await axios.post(`${config.get('url')}message`, payload);
this.snack(`Message sent to ${app.name}`);
};

Expand Down
Loading