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

mendersoftware / mender-server / 1495380963

14 Oct 2024 03:35PM UTC coverage: 70.373% (-2.5%) from 72.904%
1495380963

Pull #101

gitlab-ci

mineralsfree
feat: tenant list added

Ticket: MEN-7568
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #101: feat: tenant list added

4406 of 6391 branches covered (68.94%)

Branch coverage included in aggregate %.

88 of 183 new or added lines in 10 files covered. (48.09%)

2623 existing lines in 65 files now uncovered.

36673 of 51982 relevant lines covered (70.55%)

31.07 hits per line

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

93.46
/backend/services/deviceconnect/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/mender-server/pkg/identity"
28

29
        "github.com/mendersoftware/mender-server/services/deviceconnect/client/workflows"
30
        "github.com/mendersoftware/mender-server/services/deviceconnect/model"
31
        "github.com/mendersoftware/mender-server/services/deviceconnect/store"
32
)
33

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

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

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

77
type Config struct {
78
        HaveAuditLogs bool
79
}
80

81
// NewApp initialize a new deviceconnect App
82
func New(ds store.DataStore, wf workflows.Client, config ...Config) App {
1✔
83
        conf := Config{}
1✔
84
        for _, cfgIn := range config {
2✔
85
                if cfgIn.HaveAuditLogs {
2✔
86
                        conf.HaveAuditLogs = true
1✔
87
                }
1✔
88
        }
89
        return &app{
1✔
90
                store:            ds,
1✔
91
                workflows:        wf,
1✔
92
                Config:           conf,
1✔
93
                shutdownCancels:  make(map[uint32]context.CancelFunc),
1✔
94
                shutdownCancelsM: &sync.Mutex{},
1✔
95
                shutdownDone:     make(chan struct{}),
1✔
96
        }
1✔
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) {
1✔
119
        device, err := a.store.GetDevice(ctx, tenantID, deviceID)
1✔
120
        if err != nil {
2✔
121
                return nil, err
1✔
122
        } else if device == nil {
3✔
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,
UNCOV
137
) (int64, error) {
×
UNCOV
138
        return a.store.SetDeviceConnected(ctx, tenantID, deviceID)
×
UNCOV
139
}
×
140
func (a *app) SetDeviceDisconnected(
141
        ctx context.Context,
142
        tenantID string,
143
        deviceID string,
144
        version int64,
UNCOV
145
) error {
×
UNCOV
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 {
1✔
154
        if sess == nil {
2✔
155
                return errors.New("nil Session")
1✔
156
        }
1✔
157
        if sess.ID == "" {
2✔
158
                sessID, err := uuid.NewRandom()
1✔
159
                if err != nil {
2✔
160
                        return errors.Wrap(err, "failed to generate session ID")
1✔
161
                }
1✔
162
                sess.ID = sessID.String()
1✔
163
        }
164
        if err := sess.Validate(); err != nil {
2✔
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)
1✔
169
        if err != nil {
2✔
170
                return err
1✔
171
        } else if device == nil {
3✔
172
                return ErrDeviceNotFound
1✔
173
        } else if device.Status != model.DeviceStatusConnected {
3✔
174
                return ErrDeviceNotConnected
1✔
175
        }
1✔
176

177
        err = a.store.AllocateSession(ctx, sess)
1✔
178
        if err != nil {
2✔
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 {
1✔
191
        if !a.HaveAuditLogs {
1✔
UNCOV
192
                return nil
×
UNCOV
193
        }
×
194
        var change string
1✔
195
        var action workflows.Action
1✔
196
        if sessionType == model.SessionTypePortForward {
2✔
197
                change = "User requested a new port forwarding session"
1✔
198
                action = workflows.ActionPortForwardOpen
1✔
199
        } else if sessionType == model.SessionTypeTerminal {
3✔
200
                change = "User requested a new terminal session"
1✔
201
                action = workflows.ActionTerminalOpen
1✔
202
        } else {
1✔
203
                return errors.New("unknown session type: " + sessionType)
×
204
        }
×
205
        err := a.workflows.SubmitAuditLog(ctx, workflows.AuditLog{
1✔
206
                Action: action,
1✔
207
                Actor: workflows.Actor{
1✔
208
                        ID:   sess.UserID,
1✔
209
                        Type: workflows.ActorUser,
1✔
210
                },
1✔
211
                Object: workflows.Object{
1✔
212
                        ID:   sess.DeviceID,
1✔
213
                        Type: workflows.ObjectDevice,
1✔
214
                },
1✔
215
                Change: change,
1✔
216
                MetaData: map[string][]string{
1✔
217
                        "session_id": {sess.ID},
1✔
218
                },
1✔
219
                EventTS: time.Now(),
1✔
220
        })
1✔
221
        if err != nil {
2✔
222
                err = errors.Wrap(err, "failed to submit audit log")
1✔
223
                _, e := a.store.DeleteSession(ctx, sess.ID)
1✔
224
                if e != nil {
2✔
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
1✔
231
        }
232
        return nil
1✔
233
}
234

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

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

282
func (a *app) SaveSessionRecording(ctx context.Context, id string, sessionBytes []byte) error {
1✔
283
        err := a.store.InsertSessionRecording(ctx, id, sessionBytes)
1✔
284
        return err
1✔
285
}
1✔
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

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

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

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

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

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

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

349
var shutdownID uint32
350

351
func (a *app) RegisterShutdownCancel(cancel context.CancelFunc) uint32 {
1✔
352
        a.shutdownCancelsM.Lock()
1✔
353
        defer a.shutdownCancelsM.Unlock()
1✔
354
        id := atomic.AddUint32(&shutdownID, 1)
1✔
355
        a.shutdownCancels[id] = cancel
1✔
356
        return id
1✔
357
}
1✔
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✔
364

365
func (d *app) DeleteTenant(ctx context.Context, tenantID string) error {
1✔
366
        tenantCtx := identity.WithContext(ctx, &identity.Identity{
1✔
367
                Tenant: tenantID,
1✔
368
        })
1✔
369
        return d.store.DeleteTenant(tenantCtx, tenantID)
1✔
370
}
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