• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / deviceconnect / 1411272355

19 Jun 2024 08:35AM UTC coverage: 76.516% (-1.4%) from 77.947%
1411272355

push

gitlab-ci

web-flow
Merge pull request #378 from alfrunes/1.5.x

Cherry-pick #376 (MEN-7333) to 1.5.x

41 of 45 new or added lines in 3 files covered. (91.11%)

2 existing lines in 1 file now uncovered.

2372 of 3100 relevant lines covered (76.52%)

22.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.3
/app/app.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package app
16

17
import (
18
        "context"
19
        "io"
20
        "sync"
21
        "sync/atomic"
22
        "time"
23

24
        "github.com/google/uuid"
25
        "github.com/pkg/errors"
26

27
        "github.com/mendersoftware/deviceconnect/client/inventory"
28
        "github.com/mendersoftware/deviceconnect/client/workflows"
29
        "github.com/mendersoftware/deviceconnect/model"
30
        "github.com/mendersoftware/deviceconnect/store"
31
)
32

33
// App errors
34
var (
35
        ErrDeviceNotFound     = errors.New("device not found")
36
        ErrDeviceNotConnected = errors.New("device not connected")
37
)
38

39
// App interface describes app objects
40
//
41
//nolint:lll
42
//go:generate ../utils/mockgen.sh
43
type App interface {
44
        HealthCheck(ctx context.Context) error
45
        ProvisionDevice(ctx context.Context, tenantID string, device *model.Device) error
46
        GetDevice(ctx context.Context, tenantID, deviceID string) (*model.Device, error)
47
        DeleteDevice(ctx context.Context, tenantID, deviceID string) error
48
        SetDeviceConnected(ctx context.Context, tenantID, deviceID string) (int64, error)
49
        SetDeviceDisconnected(ctx context.Context, tenantID, deviceID string, version int64) error
50
        PrepareUserSession(ctx context.Context, sess *model.Session) error
51
        LogUserSession(ctx context.Context, sess *model.Session, sessionType string) error
52
        FreeUserSession(ctx context.Context, sessionID string, sessionTypes []string) error
53
        GetSessionRecording(ctx context.Context, id string, w io.Writer) (err error)
54
        SaveSessionRecording(ctx context.Context, id string, sessionBytes []byte) error
55
        GetRecorder(ctx context.Context, sessionID string) io.Writer
56
        GetControlRecorder(ctx context.Context, sessionID string) io.Writer
57
        DownloadFile(ctx context.Context, userID string, deviceID string, path string) error
58
        UploadFile(ctx context.Context, userID string, deviceID string, path string) error
59
        Shutdown(timeout time.Duration)
60
        ShutdownDone()
61
        RegisterShutdownCancel(context.CancelFunc) uint32
62
        UnregisterShutdownCancel(uint32)
63
}
64

65
// app is an app object
66
type app struct {
67
        store            store.DataStore
68
        inventory        inventory.Client
69
        workflows        workflows.Client
70
        shutdownCancels  map[uint32]context.CancelFunc
71
        shutdownCancelsM *sync.Mutex
72
        shutdownDone     chan struct{}
73
        Config
74
}
75

76
type Config struct {
77
        HaveAuditLogs bool
78
}
79

80
// NewApp initialize a new deviceconnect App
81
func New(ds store.DataStore, inv inventory.Client, wf workflows.Client, config ...Config) App {
34✔
82
        conf := Config{}
34✔
83
        for _, cfgIn := range config {
59✔
84
                if cfgIn.HaveAuditLogs {
37✔
85
                        conf.HaveAuditLogs = true
12✔
86
                }
12✔
87
        }
88
        return &app{
34✔
89
                store:            ds,
34✔
90
                inventory:        inv,
34✔
91
                workflows:        wf,
34✔
92
                Config:           conf,
34✔
93
                shutdownCancels:  make(map[uint32]context.CancelFunc),
34✔
94
                shutdownCancelsM: &sync.Mutex{},
34✔
95
                shutdownDone:     make(chan struct{}),
34✔
96
        }
34✔
97
}
98

99
// HealthCheck performs a health check and returns an error if it fails
100
func (a *app) HealthCheck(ctx context.Context) error {
1✔
101
        return a.store.Ping(ctx)
1✔
102
}
1✔
103

104
// ProvisionDevice provisions a new tenant
105
func (a *app) ProvisionDevice(
106
        ctx context.Context,
107
        tenantID string,
108
        device *model.Device,
109
) error {
1✔
110
        return a.store.ProvisionDevice(ctx, tenantID, device.ID)
1✔
111
}
1✔
112

113
// GetDevice returns a device
114
func (a *app) GetDevice(
115
        ctx context.Context,
116
        tenantID string,
117
        deviceID string,
118
) (*model.Device, error) {
3✔
119
        device, err := a.store.GetDevice(ctx, tenantID, deviceID)
3✔
120
        if err != nil {
4✔
121
                return nil, err
1✔
122
        } else if device == nil {
4✔
123
                return nil, ErrDeviceNotFound
1✔
124
        }
1✔
125
        return device, nil
1✔
126
}
127

128
// DeleteDevice provisions a new tenant
129
func (a *app) DeleteDevice(ctx context.Context, tenantID, deviceID string) error {
1✔
130
        return a.store.DeleteDevice(ctx, tenantID, deviceID)
1✔
131
}
1✔
132

133
func (a *app) SetDeviceConnected(
134
        ctx context.Context,
135
        tenantID string,
136
        deviceID string,
NEW
137
) (int64, error) {
×
NEW
138
        return a.store.SetDeviceConnected(ctx, tenantID, deviceID)
×
NEW
139
}
×
140
func (a *app) SetDeviceDisconnected(
141
        ctx context.Context,
142
        tenantID string,
143
        deviceID string,
144
        version int64,
UNCOV
145
) error {
×
NEW
146
        return a.store.SetDeviceDisconnected(ctx, tenantID, deviceID, version)
×
UNCOV
147
}
×
148

149
// PrepareUserSession prepares a new user session
150
func (a *app) PrepareUserSession(
151
        ctx context.Context,
152
        sess *model.Session,
153
) error {
8✔
154
        if sess == nil {
9✔
155
                return errors.New("nil Session")
1✔
156
        }
1✔
157
        if sess.ID == "" {
14✔
158
                sessID, err := uuid.NewRandom()
7✔
159
                if err != nil {
8✔
160
                        return errors.Wrap(err, "failed to generate session ID")
1✔
161
                }
1✔
162
                sess.ID = sessID.String()
6✔
163
        }
164
        if err := sess.Validate(); err != nil {
7✔
165
                return errors.Wrap(err, "app: cannot create invalid Session")
1✔
166
        }
1✔
167

168
        device, err := a.store.GetDevice(ctx, sess.TenantID, sess.DeviceID)
5✔
169
        if err != nil {
6✔
170
                return err
1✔
171
        } else if device == nil {
6✔
172
                return ErrDeviceNotFound
1✔
173
        } else if device.Status != model.DeviceStatusConnected {
5✔
174
                return ErrDeviceNotConnected
1✔
175
        }
1✔
176

177
        err = a.store.AllocateSession(ctx, sess)
2✔
178
        if err != nil {
3✔
179
                return err
1✔
180
        }
1✔
181

182
        return nil
1✔
183
}
184

185
// LogUserSession logs a new user session
186
func (a *app) LogUserSession(
187
        ctx context.Context,
188
        sess *model.Session,
189
        sessionType string,
190
) error {
4✔
191
        if !a.HaveAuditLogs {
4✔
192
                return nil
×
193
        }
×
194
        var change string
4✔
195
        var action workflows.Action
4✔
196
        if sessionType == model.SessionTypePortForward {
5✔
197
                change = "User requested a new port forwarding session"
1✔
198
                action = workflows.ActionPortForwardOpen
1✔
199
        } else if sessionType == model.SessionTypeTerminal {
7✔
200
                change = "User requested a new terminal session"
3✔
201
                action = workflows.ActionTerminalOpen
3✔
202
        } else {
3✔
203
                return errors.New("unknown session type: " + sessionType)
×
204
        }
×
205
        err := a.workflows.SubmitAuditLog(ctx, workflows.AuditLog{
4✔
206
                Action: action,
4✔
207
                Actor: workflows.Actor{
4✔
208
                        ID:   sess.UserID,
4✔
209
                        Type: workflows.ActorUser,
4✔
210
                },
4✔
211
                Object: workflows.Object{
4✔
212
                        ID:   sess.DeviceID,
4✔
213
                        Type: workflows.ObjectDevice,
4✔
214
                },
4✔
215
                Change: change,
4✔
216
                MetaData: map[string][]string{
4✔
217
                        "session_id": {sess.ID},
4✔
218
                },
4✔
219
                EventTS: time.Now(),
4✔
220
        })
4✔
221
        if err != nil {
6✔
222
                err = errors.Wrap(err, "failed to submit audit log")
2✔
223
                _, e := a.store.DeleteSession(ctx, sess.ID)
2✔
224
                if e != nil {
3✔
225
                        err = errors.Errorf(
1✔
226
                                "%s: failed to clean up session state: %s",
1✔
227
                                err.Error(), e.Error(),
1✔
228
                        )
1✔
229
                }
1✔
230
                return err
2✔
231
        }
232
        return nil
2✔
233
}
234

235
// FreeUserSession releases the session
236
func (a *app) FreeUserSession(
237
        ctx context.Context,
238
        sessionID string,
239
        sessionTypes []string,
240
) error {
5✔
241
        sess, err := a.store.DeleteSession(ctx, sessionID)
5✔
242
        if err != nil {
6✔
243
                return err
1✔
244
        }
1✔
245
        if a.HaveAuditLogs {
7✔
246
                for _, sessionType := range sessionTypes {
6✔
247
                        var action workflows.Action
3✔
248
                        if sessionType == model.SessionTypePortForward {
4✔
249
                                action = workflows.ActionPortForwardClose
1✔
250
                        } else if sessionType == model.SessionTypeTerminal {
5✔
251
                                action = workflows.ActionTerminalClose
2✔
252
                        } else {
2✔
253
                                continue
×
254
                        }
255
                        err = a.workflows.SubmitAuditLog(ctx, workflows.AuditLog{
3✔
256
                                Action: action,
3✔
257
                                Actor: workflows.Actor{
3✔
258
                                        ID:   sess.UserID,
3✔
259
                                        Type: workflows.ActorUser,
3✔
260
                                },
3✔
261
                                Object: workflows.Object{
3✔
262
                                        ID:   sess.DeviceID,
3✔
263
                                        Type: workflows.ObjectDevice,
3✔
264
                                },
3✔
265
                                MetaData: map[string][]string{
3✔
266
                                        "session_id": {sess.ID},
3✔
267
                                },
3✔
268
                        })
3✔
269
                        if err != nil {
4✔
270
                                return errors.Wrap(err, "failed to submit audit log")
1✔
271
                        }
1✔
272
                }
273
        }
274
        return nil
3✔
275
}
276

277
func (a *app) GetSessionRecording(ctx context.Context, id string, w io.Writer) (err error) {
2✔
278
        err = a.store.WriteSessionRecords(ctx, id, w)
2✔
279
        return err
2✔
280
}
2✔
281

282
func (a *app) SaveSessionRecording(ctx context.Context, id string, sessionBytes []byte) error {
2✔
283
        err := a.store.InsertSessionRecording(ctx, id, sessionBytes)
2✔
284
        return err
2✔
285
}
2✔
286

287
func (a app) GetRecorder(ctx context.Context, sessionID string) io.Writer {
1✔
288
        return NewRecorder(ctx, sessionID, a.store)
1✔
289
}
1✔
290

291
func (a app) GetControlRecorder(ctx context.Context, sessionID string) io.Writer {
×
292
        return NewControlRecorder(ctx, sessionID, a.store)
×
293
}
×
294

295
func (a *app) DownloadFile(ctx context.Context, userID string, deviceID string, path string) error {
3✔
296
        return a.submitFileTransferAuditlog(ctx, userID, deviceID, path,
3✔
297
                workflows.ActionDownloadFile, "User downloaded a file from the device")
3✔
298
}
3✔
299

300
func (a *app) UploadFile(ctx context.Context, userID string, deviceID string, path string) error {
3✔
301
        return a.submitFileTransferAuditlog(ctx, userID, deviceID, path,
3✔
302
                workflows.ActionUploadFile, "User uploaded a file to the device")
3✔
303
}
3✔
304

305
func (a *app) submitFileTransferAuditlog(ctx context.Context, userID string, deviceID string,
306
        path string, action workflows.Action, change string) error {
6✔
307
        if a.HaveAuditLogs {
10✔
308
                err := a.workflows.SubmitAuditLog(ctx, workflows.AuditLog{
4✔
309
                        Action: action,
4✔
310
                        Actor: workflows.Actor{
4✔
311
                                ID:   userID,
4✔
312
                                Type: workflows.ActorUser,
4✔
313
                        },
4✔
314
                        Object: workflows.Object{
4✔
315
                                ID:   deviceID,
4✔
316
                                Type: workflows.ObjectDevice,
4✔
317
                        },
4✔
318
                        Change: change,
4✔
319
                        MetaData: map[string][]string{
4✔
320
                                "path": {path},
4✔
321
                        },
4✔
322
                        EventTS: time.Now(),
4✔
323
                })
4✔
324
                if err != nil {
6✔
325
                        return errors.Wrap(err,
2✔
326
                                "failed to submit audit log for file transfer",
2✔
327
                        )
2✔
328
                }
2✔
329
        }
330
        return nil
4✔
331
}
332

333
func (a *app) Shutdown(timeout time.Duration) {
2✔
334
        a.shutdownCancelsM.Lock()
2✔
335
        defer a.shutdownCancelsM.Unlock()
2✔
336
        ticker := time.NewTicker(timeout / time.Duration(len(a.shutdownCancels)+1))
2✔
337
        for _, cancel := range a.shutdownCancels {
4✔
338
                cancel()
2✔
339
                <-ticker.C
2✔
340
        }
2✔
341
        <-ticker.C
2✔
342
        close(a.shutdownDone)
2✔
343
}
344

345
func (a *app) ShutdownDone() {
2✔
346
        <-a.shutdownDone
2✔
347
}
2✔
348

349
var shutdownID uint32
350

351
func (a *app) RegisterShutdownCancel(cancel context.CancelFunc) uint32 {
3✔
352
        a.shutdownCancelsM.Lock()
3✔
353
        defer a.shutdownCancelsM.Unlock()
3✔
354
        id := atomic.AddUint32(&shutdownID, 1)
3✔
355
        a.shutdownCancels[id] = cancel
3✔
356
        return id
3✔
357
}
3✔
358

359
func (a *app) UnregisterShutdownCancel(id uint32) {
1✔
360
        a.shutdownCancelsM.Lock()
1✔
361
        defer a.shutdownCancelsM.Unlock()
1✔
362
        delete(a.shutdownCancels, id)
1✔
363
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc