Skip to content

Commit 5f167eb

Browse files
authored
Merge pull request #959 from gotify/expiry
Delete inactive clients/sessions
2 parents ec8ce07 + 87db368 commit 5f167eb

43 files changed

Lines changed: 701 additions & 160 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_test.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func (s *ApplicationSuite) Test_CreateApplication_mapAllParameters() {
7878
Name: "custom_name",
7979
Description: "description_text",
8080
SortKey: "a5",
81+
CreatedAt: testdb.Now,
8182
}
8283
assert.Equal(s.T(), 200, s.recorder.Code)
8384
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
@@ -96,8 +97,9 @@ func (s *ApplicationSuite) Test_ensureApplicationHasCorrectJsonRepresentation()
9697
Internal: true,
9798
LastUsed: nil,
9899
SortKey: "a1",
100+
CreatedAt: testdb.Now,
99101
}
100-
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true, "defaultPriority":0, "lastUsed":null, "sortKey":"a1"}`)
102+
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true, "defaultPriority":0, "createdAt":"2020-01-01T00:00:00Z", "lastUsed":null, "sortKey":"a1"}`)
101103
}
102104

103105
func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnEmptyName() {
@@ -129,19 +131,19 @@ func (s *ApplicationSuite) Test_CreateApplication_ignoresReadOnlyPropertiesInPar
129131

130132
s.a.CreateApplication(s.ctx)
131133

132-
expectedJSONValue, _ := json.Marshal(&model.Application{
134+
expected := &model.Application{
133135
ID: 1,
134136
Token: firstApplicationToken,
135-
UserID: 5,
136137
Name: "name",
137138
Description: "description",
138139
Internal: false,
139140
Image: "static/defaultapp.png",
140141
SortKey: "a5",
141-
})
142+
CreatedAt: testdb.Now,
143+
}
142144

143145
assert.Equal(s.T(), 200, s.recorder.Code)
144-
assert.Equal(s.T(), string(expectedJSONValue), s.recorder.Body.String())
146+
test.BodyEquals(s.T(), expected, s.recorder)
145147
}
146148

147149
func (s *ApplicationSuite) Test_DeleteApplication_expectNotFoundOnCurrentUserIsNotOwner() {
@@ -165,7 +167,7 @@ func (s *ApplicationSuite) Test_CreateApplication_onlyRequiredParameters() {
165167
s.withFormData("name=custom_name")
166168
s.a.CreateApplication(s.ctx)
167169

168-
expected := &model.Application{ID: 1, Token: firstApplicationToken, Name: "custom_name", UserID: 5, SortKey: "a0"}
170+
expected := &model.Application{ID: 1, Token: firstApplicationToken, Name: "custom_name", UserID: 5, SortKey: "a0", CreatedAt: testdb.Now}
169171
assert.Equal(s.T(), 200, s.recorder.Code)
170172
if app, err := s.db.GetApplicationsByUser(5); assert.NoError(s.T(), err) {
171173
assert.Contains(s.T(), app, expected)
@@ -181,12 +183,12 @@ func (s *ApplicationSuite) Test_CreateApplication_returnsApplicationWithID() {
181183
s.a.CreateApplication(s.ctx)
182184

183185
expected := &model.Application{
184-
ID: 1,
185-
Token: firstApplicationToken,
186-
Name: "custom_name",
187-
Image: "static/defaultapp.png",
188-
UserID: 5,
189-
SortKey: "a0",
186+
ID: 1,
187+
Token: firstApplicationToken,
188+
Name: "custom_name",
189+
Image: "static/defaultapp.png",
190+
SortKey: "a0",
191+
CreatedAt: testdb.Now,
190192
}
191193
assert.Equal(s.T(), 200, s.recorder.Code)
192194
test.BodyEquals(s.T(), expected, s.recorder)
@@ -201,7 +203,7 @@ func (s *ApplicationSuite) Test_CreateApplication_withExistingToken() {
201203

202204
s.a.CreateApplication(s.ctx)
203205

204-
expected := &model.Application{ID: 2, Token: secondApplicationToken, Name: "custom_name", UserID: 5, SortKey: "a0"}
206+
expected := &model.Application{ID: 2, Token: secondApplicationToken, Name: "custom_name", UserID: 5, SortKey: "a0", CreatedAt: testdb.Now}
205207
assert.Equal(s.T(), 200, s.recorder.Code)
206208
if app, err := s.db.GetApplicationsByUser(5); assert.NoError(s.T(), err) {
207209
assert.Contains(s.T(), app, expected)
@@ -530,6 +532,7 @@ func (s *ApplicationSuite) Test_UpdateApplicationNameAndDescription_expectSucces
530532
Name: "new_name",
531533
Description: "new_description_text",
532534
SortKey: "a0",
535+
CreatedAt: testdb.Now,
533536
}
534537

535538
assert.Equal(s.T(), 200, s.recorder.Code)
@@ -553,6 +556,7 @@ func (s *ApplicationSuite) Test_UpdateApplicationName_expectSuccess() {
553556
Name: "new_name",
554557
Description: "",
555558
SortKey: "a0",
559+
CreatedAt: testdb.Now,
556560
}
557561

558562
assert.Equal(s.T(), 200, s.recorder.Code)
@@ -577,6 +581,7 @@ func (s *ApplicationSuite) Test_UpdateApplicationDefaultPriority_expectSuccess()
577581
Description: "",
578582
DefaultPriority: 4,
579583
SortKey: "a0",
584+
CreatedAt: testdb.Now,
580585
}
581586

582587
assert.Equal(s.T(), 200, s.recorder.Code)

api/client.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ type ClientParams struct {
3939
// required: true
4040
// example: My Client
4141
Name string `form:"name" query:"name" json:"name" binding:"required"`
42+
// The number of seconds of inactivity after which the client is removed.
43+
// 0 (or omitted) means the client never expires.
44+
//
45+
// example: 2592000
46+
ExpiresAfterInactivitySeconds *uint `form:"expiresAfterInactivitySeconds" query:"expiresAfterInactivitySeconds" json:"expiresAfterInactivitySeconds"`
4247
}
4348

4449
// UpdateClient updates a client by its id.
@@ -94,6 +99,9 @@ func (a *ClientAPI) UpdateClient(ctx *gin.Context) {
9499
newValues := ClientParams{}
95100
if err := ctx.Bind(&newValues); err == nil {
96101
client.Name = newValues.Name
102+
if newValues.ExpiresAfterInactivitySeconds != nil {
103+
client.ExpiresAfterInactivitySeconds = *newValues.ExpiresAfterInactivitySeconds
104+
}
97105

98106
if success := successOrAbort(ctx, 500, a.DB.UpdateClient(client)); !success {
99107
return
@@ -147,6 +155,9 @@ func (a *ClientAPI) CreateClient(ctx *gin.Context) {
147155
Token: auth.GenerateNotExistingToken(generateClientToken, a.clientExists),
148156
UserID: auth.GetUserID(ctx),
149157
}
158+
if clientParams.ExpiresAfterInactivitySeconds != nil {
159+
client.ExpiresAfterInactivitySeconds = *clientParams.ExpiresAfterInactivitySeconds
160+
}
150161

151162
if success := successOrAbort(ctx, 500, a.DB.CreateClient(&client)); !success {
152163
return

api/client_test.go

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ func (s *ClientSuite) AfterTest(suiteName, testName string) {
5959
}
6060

6161
func (s *ClientSuite) Test_ensureClientHasCorrectJsonRepresentation() {
62-
actual := &model.Client{ID: 1, UserID: 2, Token: "Casdasfgeeg", Name: "myclient"}
63-
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Casdasfgeeg","name":"myclient","lastUsed":null}`)
62+
actual := &model.Client{ID: 1, UserID: 2, Token: "Casdasfgeeg", Name: "myclient", CreatedAt: testdb.Now}
63+
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Casdasfgeeg","name":"myclient","createdAt":"2020-01-01T00:00:00Z","lastUsed":null,"expiresAfterInactivitySeconds":0}`)
6464
}
6565

6666
func (s *ClientSuite) Test_CreateClient_mapAllParameters() {
@@ -71,7 +71,7 @@ func (s *ClientSuite) Test_CreateClient_mapAllParameters() {
7171

7272
s.a.CreateClient(s.ctx)
7373

74-
expected := &model.Client{ID: 1, Token: firstClientToken, UserID: 5, Name: "custom_name"}
74+
expected := &model.Client{ID: 1, Token: firstClientToken, UserID: 5, Name: "custom_name", CreatedAt: testdb.Now}
7575
assert.Equal(s.T(), 200, s.recorder.Code)
7676
if clients, err := s.db.GetClientsByUser(5); assert.NoError(s.T(), err) {
7777
assert.Contains(s.T(), clients, expected)
@@ -85,7 +85,7 @@ func (s *ClientSuite) Test_CreateClient_ignoresReadOnlyPropertiesInParams() {
8585
s.withFormData("name=myclient&ID=45&Token=12341234&UserID=333")
8686

8787
s.a.CreateClient(s.ctx)
88-
expected := &model.Client{ID: 1, UserID: 5, Token: firstClientToken, Name: "myclient"}
88+
expected := &model.Client{ID: 1, UserID: 5, Token: firstClientToken, Name: "myclient", CreatedAt: testdb.Now}
8989

9090
assert.Equal(s.T(), 200, s.recorder.Code)
9191
if clients, err := s.db.GetClientsByUser(5); assert.NoError(s.T(), err) {
@@ -129,7 +129,7 @@ func (s *ClientSuite) Test_CreateClient_returnsClientWithID() {
129129

130130
s.a.CreateClient(s.ctx)
131131

132-
expected := &model.Client{ID: 1, Token: firstClientToken, Name: "custom_name", UserID: 5}
132+
expected := &model.Client{ID: 1, Token: firstClientToken, Name: "custom_name", CreatedAt: testdb.Now}
133133
assert.Equal(s.T(), 200, s.recorder.Code)
134134
test.BodyEquals(s.T(), expected, s.recorder)
135135
}
@@ -142,7 +142,7 @@ func (s *ClientSuite) Test_CreateClient_withExistingToken() {
142142

143143
s.a.CreateClient(s.ctx)
144144

145-
expected := &model.Client{ID: 2, Token: secondClientToken, Name: "custom_name", UserID: 5}
145+
expected := &model.Client{ID: 2, Token: secondClientToken, Name: "custom_name", CreatedAt: testdb.Now}
146146
assert.Equal(s.T(), 200, s.recorder.Code)
147147
test.BodyEquals(s.T(), expected, s.recorder)
148148
}
@@ -189,6 +189,34 @@ func (s *ClientSuite) Test_DeleteClient() {
189189
assert.True(s.T(), s.notified)
190190
}
191191

192+
func (s *ClientSuite) Test_CreateClient_acceptsExpiresAfterInactivitySeconds() {
193+
s.db.User(5)
194+
195+
test.WithUser(s.ctx, 5)
196+
s.withFormData("name=custom_name&expiresAfterInactivitySeconds=3600")
197+
198+
s.a.CreateClient(s.ctx)
199+
200+
assert.Equal(s.T(), 200, s.recorder.Code)
201+
if client, err := s.db.GetClientByID(1); assert.NoError(s.T(), err) {
202+
assert.Equal(s.T(), uint(3600), client.ExpiresAfterInactivitySeconds)
203+
}
204+
}
205+
206+
func (s *ClientSuite) Test_UpdateClient_updatesExpiresAfterInactivitySeconds() {
207+
s.db.User(5).NewClientWithToken(1, firstClientToken)
208+
209+
test.WithUser(s.ctx, 5)
210+
s.withFormData("name=firefox&expiresAfterInactivitySeconds=7200")
211+
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
212+
s.a.UpdateClient(s.ctx)
213+
214+
assert.Equal(s.T(), 200, s.recorder.Code)
215+
if client, err := s.db.GetClientByID(1); assert.NoError(s.T(), err) {
216+
assert.Equal(s.T(), uint(7200), client.ExpiresAfterInactivitySeconds)
217+
}
218+
}
219+
192220
func (s *ClientSuite) Test_UpdateClient_expectSuccess() {
193221
s.db.User(5).NewClientWithToken(1, firstClientToken)
194222

@@ -198,10 +226,11 @@ func (s *ClientSuite) Test_UpdateClient_expectSuccess() {
198226
s.a.UpdateClient(s.ctx)
199227

200228
expected := &model.Client{
201-
ID: 1,
202-
Token: firstClientToken,
203-
UserID: 5,
204-
Name: "firefox",
229+
ID: 1,
230+
Token: firstClientToken,
231+
UserID: 5,
232+
Name: "firefox",
233+
CreatedAt: testdb.Now,
205234
}
206235

207236
assert.Equal(s.T(), 200, s.recorder.Code)

api/oidc.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,11 @@ func (a *OIDCAPI) resolveUser(info *oidc.UserInfo) (*model.User, int, error) {
424424
func (a *OIDCAPI) createClient(name string, userID uint) (*model.Client, error) {
425425
elevatedUntil := time.Now().Add(model.DefaultElevationDuration)
426426
client := &model.Client{
427-
Name: name,
428-
Token: auth.GenerateNotExistingToken(generateClientToken, func(t string) bool { c, _ := a.DB.GetClientByToken(t); return c != nil }),
429-
UserID: userID,
430-
ElevatedUntil: &elevatedUntil,
427+
Name: name,
428+
Token: auth.GenerateNotExistingToken(generateClientToken, func(t string) bool { c, _ := a.DB.GetClientByToken(t); return c != nil }),
429+
UserID: userID,
430+
ElevatedUntil: &elevatedUntil,
431+
ExpiresAfterInactivitySeconds: auth.CookieMaxAge,
431432
}
432433
return client, a.DB.CreateClient(client)
433434
}

api/oidc_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/gin-gonic/gin"
10+
"github.com/gotify/server/v2/auth"
1011
"github.com/gotify/server/v2/decaymap"
1112
"github.com/gotify/server/v2/mode"
1213
"github.com/gotify/server/v2/test"
@@ -153,6 +154,7 @@ func (s *OIDCSuite) Test_CreateClient() {
153154
assert.Equal(s.T(), "MyPhone", client.Name)
154155
assert.Equal(s.T(), "Ctesttoken00001", client.Token)
155156
assert.Equal(s.T(), uint(1), client.UserID)
157+
assert.Equal(s.T(), uint(auth.CookieMaxAge), client.ExpiresAfterInactivitySeconds)
156158

157159
dbClient, err := s.db.GetClientByToken("Ctesttoken00001")
158160
assert.NoError(s.T(), err)

api/plugin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func (c *PluginAPI) GetPlugins(ctx *gin.Context) {
7272
info := c.Manager.PluginInfo(conf.ModulePath)
7373
result = append(result, model.PluginConfExternal{
7474
ID: conf.ID,
75+
CreatedAt: conf.CreatedAt,
7576
Name: info.String(),
7677
Token: conf.Token,
7778
ModulePath: conf.ModulePath,

api/session.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ func (a *SessionAPI) Login(ctx *gin.Context) {
7777

7878
elevatedUntil := time.Now().Add(model.DefaultElevationDuration)
7979
client := model.Client{
80-
Name: clientParams.Name,
81-
Token: auth.GenerateNotExistingToken(generateClientToken, a.clientExists),
82-
UserID: user.ID,
83-
ElevatedUntil: &elevatedUntil,
80+
Name: clientParams.Name,
81+
Token: auth.GenerateNotExistingToken(generateClientToken, a.clientExists),
82+
UserID: user.ID,
83+
ElevatedUntil: &elevatedUntil,
84+
ExpiresAfterInactivitySeconds: auth.CookieMaxAge,
8485
}
8586
if success := successOrAbort(ctx, 500, a.DB.CreateClient(&client)); !success {
8687
return
@@ -92,6 +93,7 @@ func (a *SessionAPI) Login(ctx *gin.Context) {
9293
ID: user.ID,
9394
Name: user.Name,
9495
Admin: user.Admin,
96+
CreatedAt: user.CreatedAt,
9597
ClientID: client.ID,
9698
ElevatedUntil: client.ElevatedUntil,
9799
})

api/session_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func (s *SessionSuite) Test_Login_Success() {
9090
assert.NoError(s.T(), err)
9191
assert.Len(s.T(), clients, 1)
9292
assert.Equal(s.T(), "test-browser", clients[0].Name)
93+
assert.Equal(s.T(), uint(auth.CookieMaxAge), clients[0].ExpiresAfterInactivitySeconds)
9394
}
9495

9596
func (s *SessionSuite) Test_Login_WrongPassword() {

api/user.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,10 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
132132
return
133133
}
134134
result := &model.CurrentUserExternal{
135-
ID: user.ID,
136-
Name: user.Name,
137-
Admin: user.Admin,
135+
ID: user.ID,
136+
Name: user.Name,
137+
Admin: user.Admin,
138+
CreatedAt: user.CreatedAt,
138139
}
139140
client := auth.GetClient(ctx)
140141
if client != nil {
@@ -460,10 +461,11 @@ func (a *UserAPI) UpdateUserByID(ctx *gin.Context) {
460461
return
461462
}
462463
internal := &model.User{
463-
ID: oldUser.ID,
464-
Name: user.Name,
465-
Admin: user.Admin,
466-
Pass: oldUser.Pass,
464+
ID: oldUser.ID,
465+
Name: user.Name,
466+
Admin: user.Admin,
467+
Pass: oldUser.Pass,
468+
CreatedAt: oldUser.CreatedAt,
467469
}
468470
if user.Pass != "" {
469471
internal.Pass = password.CreatePassword(user.Pass, a.PasswordStrength)
@@ -481,8 +483,9 @@ func (a *UserAPI) UpdateUserByID(ctx *gin.Context) {
481483

482484
func toExternalUser(internal *model.User) *model.UserExternal {
483485
return &model.UserExternal{
484-
Name: internal.Name,
485-
Admin: internal.Admin,
486-
ID: internal.ID,
486+
Name: internal.Name,
487+
Admin: internal.Admin,
488+
ID: internal.ID,
489+
CreatedAt: internal.CreatedAt,
487490
}
488491
}

api/user_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func (s *UserSuite) Test_CreateUser() {
176176
s.a.CreateUser(s.ctx)
177177

178178
assert.Equal(s.T(), 200, s.recorder.Code)
179-
user := &model.UserExternal{ID: 2, Name: "tom", Admin: true}
179+
user := &model.UserExternal{ID: 2, Name: "tom", Admin: true, CreatedAt: testdb.Now}
180180
test.BodyEquals(s.T(), user, s.recorder)
181181

182182
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
@@ -439,5 +439,5 @@ func (s *UserSuite) noLogin() {
439439
}
440440

441441
func externalOf(user *model.User) *model.UserExternal {
442-
return &model.UserExternal{Name: user.Name, Admin: user.Admin, ID: user.ID}
442+
return &model.UserExternal{Name: user.Name, Admin: user.Admin, ID: user.ID, CreatedAt: user.CreatedAt}
443443
}

0 commit comments

Comments
 (0)