Skip to content
Merged
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
73 changes: 73 additions & 0 deletions api/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,79 @@ func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) {
})
}

// UpdateApplicationSecurity performs security updates on an application.
// swagger:operation PUT /application/{id}/security application updateAppSecurity
//
// Perform security updates on an application.
//
// Requires elevated authentication.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: body
// in: body
// description: security update action descriptor
// required: true
// schema:
// $ref: "#/definitions/SecurityUpdateAction"
// - name: id
// in: path
// description: the application id
// required: true
// type: integer
// format: int64
// responses:
// 200:
// description: Ok
// schema:
// $ref: "#/definitions/SecurityUpdateActionResponse"
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
// 404:
// description: Not Found
// schema:
// $ref: "#/definitions/Error"
// 500:
// description: Server Error
// schema:
// $ref: "#/definitions/Error"
func (a *ApplicationAPI) UpdateApplicationSecurity(ctx *gin.Context) {
withID(ctx, "id", func(id uint) {
app, err := a.DB.GetApplicationByID(id)
if success := successOrAbort(ctx, 500, err); !success {
return
}
action := model.SecurityUpdateAction{}
response := model.SecurityUpdateActionResponse{}
if err := ctx.Bind(&action); err == nil {
if action.RegenerateToken {
tokenPublic, tokenPrivate := generateApplicationToken()
app.Token = tokenPublic
response.RegenerateToken = &model.RegenerateTokenResponse{
Token: tokenPrivate,
}
}
if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
return
}
ctx.JSON(200, response)
}
})
}

// UploadApplicationImage uploads an image for an application.
// swagger:operation POST /application/{id}/image application uploadAppImage
//
Expand Down
44 changes: 44 additions & 0 deletions api/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,50 @@ func (s *ApplicationSuite) Test_CreateApplication_ignoresReadOnlyPropertiesInPar
}
}

func (s *ApplicationSuite) Test_UpdateApplicationSecurity_regenerateToken() {
Comment thread
jmattheis marked this conversation as resolved.
s.db.User(5).App(1)
test.WithUser(s.ctx, 5)

oldToken, err := s.db.GetApplicationByID(1)
assert.NoError(s.T(), err)
s.ctx.Request = httptest.NewRequest("PUT", "/application/1/security", bytes.NewBufferString(`{"regenerateToken": true}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
s.a.UpdateApplicationSecurity(s.ctx)
assert.Equal(s.T(), 200, s.recorder.Code)
bodyBytes, err := io.ReadAll(s.recorder.Body)
assert.NoError(s.T(), err)
var got model.SecurityUpdateActionResponse
assert.NoError(s.T(), json.Unmarshal(bodyBytes, &got))
assert.Equal(s.T(), &model.SecurityUpdateActionResponse{
RegenerateToken: &model.RegenerateTokenResponse{
Token: got.RegenerateToken.Token,
},
}, &got)
newToken, err := s.db.GetApplicationByID(1)
assert.NoError(s.T(), err)
assert.NotEqual(s.T(), oldToken.Token, newToken.Token)
}

func (s *ApplicationSuite) Test_UpdateApplicationSecurity_isNoOpIfNilAction() {
s.db.User(5).App(1)
test.WithUser(s.ctx, 5)

oldToken, err := s.db.GetApplicationByID(1)
assert.NoError(s.T(), err)
s.ctx.Request = httptest.NewRequest("PUT", "/application/1/security", bytes.NewBufferString(`{}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
s.a.UpdateApplicationSecurity(s.ctx)
assert.Equal(s.T(), 200, s.recorder.Code)
bodyBytes, err := io.ReadAll(s.recorder.Body)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "{}", string(bodyBytes))
newToken, err := s.db.GetApplicationByID(1)
assert.NoError(s.T(), err)
assert.Equal(s.T(), oldToken, newToken)
}

func (s *ApplicationSuite) Test_DeleteApplication_expectNotFoundOnCurrentUserIsNotOwner() {
s.db.User(2)
s.db.User(5).App(5)
Expand Down
130 changes: 130 additions & 0 deletions docs/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,93 @@
}
}
},
"/application/{id}/security": {
"put": {
"security": [
{
"clientTokenAuthorizationHeader": []
},
{
"clientTokenHeader": []
},
{
"clientTokenQuery": []
},
{
"basicAuth": []
}
],
"description": "Requires elevated authentication.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"application"
],
"summary": "Perform security updates on an application.",
"operationId": "updateAppSecurity",
"parameters": [
{
"description": "security update action descriptor",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/SecurityUpdateAction"
}
},
{
"type": "integer",
"format": "int64",
"description": "the application id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Ok",
"schema": {
"$ref": "#/definitions/SecurityUpdateActionResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/Error"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/Error"
}
},
"500": {
"description": "Server Error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/auth/local/login": {
"post": {
"security": [
Expand Down Expand Up @@ -3192,6 +3279,49 @@
"x-go-name": "PluginConfExternal",
"x-go-package": "github.com/gotify/server/v2/model"
},
"RegenerateTokenResponse": {
"description": "The RegenerateTokenResponse holds information about the response to the regenerate token action.",
"type": "object",
"title": "RegenerateTokenResponse Model",
"required": [
"token"
],
"properties": {
"token": {
"description": "The new token.",
"type": "string",
"x-go-name": "Token",
"readOnly": true,
"example": "gtfya.e2NcJK7AenXBPIRB3S03JsBlmy0V6xP8h0hwSiAJae8"
}
},
"x-go-package": "github.com/gotify/server/v2/model"
},
"SecurityUpdateAction": {
"description": "The SecurityUpdateAction describes the details of a requested security update.",
"type": "object",
"title": "SecurityUpdateAction Model",
"properties": {
"regenerateToken": {
"description": "Whether to regenerate the token. Your client token must be elevated to perform this action.",
"type": "boolean",
"x-go-name": "RegenerateToken",
"example": true
}
},
"x-go-package": "github.com/gotify/server/v2/model"
},
"SecurityUpdateActionResponse": {
"description": "The SecurityUpdateActionResponse holds information about the response to a security update request.",
"type": "object",
"title": "SecurityUpdateActionResponse Model",
"properties": {
"regenerateToken": {
"$ref": "#/definitions/RegenerateTokenResponse"
}
},
"x-go-package": "github.com/gotify/server/v2/model"
},
"UpdateUserExternal": {
"description": "Used for updating a user.",
"type": "object",
Expand Down
37 changes: 37 additions & 0 deletions model/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package model

// SecurityUpdateAction Model
//
// The SecurityUpdateAction describes the details of a requested security update.
//
// swagger:model SecurityUpdateAction
type SecurityUpdateAction struct {
// Whether to regenerate the token. Your client token must be elevated to perform this action.
//
// example: true
RegenerateToken bool `form:"regenerateToken" query:"regenerateToken" json:"regenerateToken"`
}

// SecurityUpdateActionResponse Model
//
// The SecurityUpdateActionResponse holds information about the response to a security update request.
//
// swagger:model SecurityUpdateActionResponse
type SecurityUpdateActionResponse struct {
// The response to the regenerate token action. Only present if the regenerate token action was requested.
RegenerateToken *RegenerateTokenResponse `json:"regenerateToken,omitempty"`
}

// RegenerateTokenResponse Model
//
// The RegenerateTokenResponse holds information about the response to the regenerate token action.
//
// swagger:model RegenerateTokenResponse
type RegenerateTokenResponse struct {
// The new token.
//
// example: gtfya.e2NcJK7AenXBPIRB3S03JsBlmy0V6xP8h0hwSiAJae8
// read only: true
// required: true
Token string `json:"token"`
}
1 change: 1 addition & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
clientElevated.Use(authentication.RequireElevatedClient)
clientElevated.POST("/client/:id/elevate", clientHandler.ElevateClient)
clientElevated.DELETE("/application/:id", applicationHandler.DeleteApplication)
clientElevated.PUT("/application/:id/security", applicationHandler.UpdateApplicationSecurity)
clientElevated.DELETE("/client/:id", clientHandler.DeleteClient)
clientElevated.POST("/current/user/password", userHandler.ChangePassword)
}
Expand Down
Loading
Loading