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

mendersoftware / useradm / 1806832573

08 May 2025 07:30AM UTC coverage: 59.747% (-27.3%) from 87.019%
1806832573

Pull #439

gitlab-ci

alfrunes
chore(docker): Clean up build command for acceptance test image

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #439: :building_construction: Upgrade dependencies

2363 of 3955 relevant lines covered (59.75%)

12.76 hits per line

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

73.97
/model/user.go
1
// Copyright 2022 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 model
16

17
import (
18
        "encoding/binary"
19
        "encoding/hex"
20
        "encoding/json"
21
        "net/url"
22
        "strconv"
23
        "strings"
24
        "time"
25

26
        validation "github.com/go-ozzo/ozzo-validation/v4"
27
        "github.com/go-ozzo/ozzo-validation/v4/is"
28
        "github.com/pkg/errors"
29

30
        "github.com/mendersoftware/useradm/jwt"
31
)
32

33
const (
34
        MinPasswordLength = 8
35
)
36

37
var (
38
        ErrPasswordTooShort = errors.Errorf(
39
                "password: must be minimum %d characters long",
40
                MinPasswordLength,
41
        )
42
        ErrEmptyUpdate = errors.New("no update information provided")
43
)
44

45
type Email string
46

47
func (email *Email) UnmarshalJSON(b []byte) error {
2✔
48
        var raw string
2✔
49
        if err := json.Unmarshal(b, &raw); err != nil {
3✔
50
                return err
1✔
51
        }
1✔
52
        *email = Email(strings.ToLower(raw))
1✔
53
        return nil
1✔
54
}
55

56
func (email Email) Validate() error {
7✔
57
        return validation.Validate(string(email),
7✔
58
                lessThan4096,
7✔
59
                is.ASCII, is.EmailFormat,
7✔
60
        )
7✔
61
}
7✔
62

63
type ETag [12]byte
64

65
var (
66
        ETagNil ETag
67
)
68

69
func (t *ETag) Increment() {
1✔
70
        c := binary.BigEndian.Uint32((*t)[8:])
1✔
71
        c += 1
1✔
72
        binary.BigEndian.PutUint32((*t)[8:], c)
1✔
73
        if *t == ETagNil {
1✔
74
                t.Increment()
×
75
        }
×
76
}
77

78
func (t *ETag) UnmarshalText(b []byte) error {
4✔
79
        if t == nil {
5✔
80
                return errors.New("nil ETag")
1✔
81
        }
1✔
82
        if len(b) == 0 {
4✔
83
                // Treat an empty string as a special case
1✔
84
                *t = [12]byte{}
1✔
85
                return nil
1✔
86
        }
1✔
87
        if len(b) != 24 {
3✔
88
                return errors.New("invalid ETag length")
1✔
89
        }
1✔
90
        _, err := hex.Decode((*t)[:], b)
1✔
91
        return err
1✔
92
}
93

94
func (t ETag) MarshalText() (b []byte, err error) {
3✔
95
        b = make([]byte, 24)
3✔
96
        hex.Encode(b, t[:])
3✔
97
        return b, err
3✔
98
}
3✔
99

100
func (t ETag) String() string {
3✔
101
        b, _ := t.MarshalText()
3✔
102
        return string(b)
3✔
103
}
3✔
104

105
type User struct {
106
        // system-generated user ID
107
        ID string `json:"id" bson:"_id"`
108

109
        // ETag is the entity tag that together with ID uniquely identifies
110
        // the User document.
111
        // NOTE: The v1 API does not support ETags, so this is only used
112
        // internally for checking pre-conditions before performing updates.
113
        ETag *ETag `json:"-" bson:"etag,omitempty"`
114

115
        // user email address
116
        Email Email `json:"email" bson:"email"`
117

118
        // user password
119
        Password string `json:"password,omitempty" bson:"password"`
120

121
        // timestamp of the user creation
122
        CreatedTs *time.Time `json:"created_ts,omitempty" bson:"created_ts,omitempty"`
123

124
        // timestamp of the last user information update
125
        UpdatedTs *time.Time `json:"updated_ts,omitempty" bson:"updated_ts,omitempty"`
126

127
        // LoginTs is the timestamp of the last login for this user.
128
        LoginTs *time.Time `json:"login_ts,omitempty" bson:"login_ts,omitempty"`
129
}
130

131
func (u User) NextETag() (ret ETag) {
1✔
132
        if u.ETag == nil {
2✔
133
                u.ETag = new(ETag)
1✔
134
        }
1✔
135
        if u.CreatedTs != nil {
2✔
136
                // Weak part of the ETag
1✔
137
                lsb := uint64(u.CreatedTs.Unix())
1✔
138
                binary.BigEndian.PutUint64(ret[:8], lsb)
1✔
139
        }
1✔
140
        c := binary.BigEndian.Uint32(u.ETag[8:])
1✔
141
        c += 1
1✔
142
        binary.BigEndian.PutUint32(ret[8:], c)
1✔
143
        if ret == ETagNil {
1✔
144
                ret.Increment()
×
145
        }
×
146
        return ret
1✔
147
}
148

149
func (u User) Validate() error {
6✔
150
        if err := validation.ValidateStruct(&u,
6✔
151
                validation.Field(&u.Email, validation.Required),
6✔
152
                validation.Field(&u.Password, validation.Required, lessThan4096),
6✔
153
        ); err != nil {
9✔
154
                return err
3✔
155
        }
3✔
156
        if len(u.Password) < MinPasswordLength {
4✔
157
                return ErrPasswordTooShort
1✔
158
        }
1✔
159
        return nil
2✔
160
}
161

162
type UserInternal struct {
163
        User
164
        PasswordHash string `json:"password_hash,omitempty" bson:"-"`
165
        Propagate    *bool  `json:"propagate,omitempty" bson:"-"`
166
}
167

168
func (u UserInternal) Validate() error {
×
169
        if u.Password == "" && u.PasswordHash == "" ||
×
170
                u.Password != "" && u.PasswordHash != "" {
×
171
                return errors.New("password *or* password_hash must be provided")
×
172
        } else if u.PasswordHash != "" {
×
173
                if u.ShouldPropagate() {
×
174
                        return errors.New(
×
175
                                "password_hash is not supported with 'propagate'; use 'password' instead",
×
176
                        )
×
177
                }
×
178
                u.User.Password = u.PasswordHash
×
179
                defer func() { u.User.Password = "" }()
×
180
        }
181

182
        return validation.ValidateStruct(&u,
×
183
                validation.Field(&u.User),
×
184
        )
×
185
}
186

187
func (u UserInternal) ShouldPropagate() bool {
×
188
        return u.Propagate == nil || *u.Propagate
×
189
}
×
190

191
type UserUpdate struct {
192
        // ETag selects user ETag for the user to update
193
        // NOTE: This is the only parameter that goes into the query condition if set.
194
        // NOTE: If set to ETagNil, it will match users without an ETag.
195
        ETag *ETag `json:"-" bson:"-"`
196

197
        // ETagUpdate sets the updated ETag value. If not set, it is incremented from the
198
        // ETag field if that field is set.
199
        ETagUpdate *ETag `json:"-" bson:"etag,omitempty"`
200

201
        // user email address
202
        Email Email `json:"email,omitempty" bson:",omitempty" valid:"email"`
203

204
        // user password
205
        Password string `json:"password,omitempty" bson:"password,omitempty"`
206

207
        // user password
208
        CurrentPassword string `json:"current_password,omitempty" bson:"-"`
209

210
        // timestamp of the last user information update
211
        UpdatedTs *time.Time `json:"-" bson:"updated_ts,omitempty"`
212

213
        // token used to update the user, optional
214
        Token *jwt.Token `json:"-" bson:"-"`
215

216
        LoginTs *time.Time `json:"-" bson:"login_ts,omitempty"`
217
}
218

219
func (u UserUpdate) Validate() error {
×
220
        if u.Email == "" && u.Password == "" {
×
221
                return ErrEmptyUpdate
×
222
        }
×
223

224
        if err := validation.ValidateStruct(&u,
×
225
                validation.Field(&u.Email),
×
226
                validation.Field(&u.Password,
×
227
                        validation.When(len(u.Password) > 0, lessThan4096),
×
228
                ),
×
229
        ); err != nil {
×
230
                return err
×
231
        }
×
232

233
        if len(u.Password) > 0 && len(u.Password) < MinPasswordLength {
×
234
                return ErrPasswordTooShort
×
235
        }
×
236
        return nil
×
237
}
238

239
type UserFilter struct {
240
        ID    []string `json:"id,omitempty"`
241
        Email []Email  `json:"email,omitempty"`
242

243
        CreatedAfter  *time.Time `json:"created_after,omitempty"`
244
        CreatedBefore *time.Time `json:"created_before,omitempty"`
245

246
        UpdatedAfter  *time.Time `json:"updated_after,omitempty"`
247
        UpdatedBefore *time.Time `json:"updated_before,omitempty"`
248
}
249

250
func (fltr *UserFilter) ParseForm(form url.Values) error {
5✔
251
        if ids, ok := form["id"]; ok {
6✔
252
                fltr.ID = ids
1✔
253
        }
1✔
254
        if emails, ok := form["email"]; ok {
6✔
255
                fltr.Email = make([]Email, len(emails))
1✔
256
                for i := range emails {
4✔
257
                        fltr.Email[i] = Email(strings.ToLower(emails[i]))
3✔
258
                }
3✔
259
        }
260
        if ca := form.Get("created_after"); ca != "" {
7✔
261
                caInt, err := strconv.ParseInt(ca, 10, 64)
2✔
262
                if err != nil {
3✔
263
                        return errors.Wrap(err,
1✔
264
                                `invalid form parameter "created_after"`)
1✔
265
                }
1✔
266
                caUnix := time.Unix(caInt, 0)
1✔
267
                fltr.CreatedAfter = &caUnix
1✔
268
        }
269

270
        if cb := form.Get("created_before"); cb != "" {
6✔
271
                cbInt, err := strconv.ParseInt(cb, 10, 64)
2✔
272
                if err != nil {
3✔
273
                        return errors.Wrap(err,
1✔
274
                                `invalid form parameter "created_before"`)
1✔
275
                }
1✔
276
                cbUnix := time.Unix(cbInt, 0)
1✔
277
                fltr.CreatedBefore = &cbUnix
1✔
278
        }
279

280
        if ua := form.Get("updated_after"); ua != "" {
5✔
281
                uaInt, err := strconv.ParseInt(ua, 10, 64)
2✔
282
                if err != nil {
3✔
283
                        return errors.Wrap(err,
1✔
284
                                `invalid form parameter "updated_after"`)
1✔
285
                }
1✔
286
                uaUnix := time.Unix(uaInt, 0)
1✔
287
                fltr.UpdatedAfter = &uaUnix
1✔
288
        }
289

290
        if ub := form.Get("updated_before"); ub != "" {
4✔
291
                ubInt, err := strconv.ParseInt(ub, 10, 64)
2✔
292
                if err != nil {
3✔
293
                        return errors.Wrap(err,
1✔
294
                                `invalid form parameter "updated_before"`)
1✔
295
                }
1✔
296
                ubUnix := time.Unix(ubInt, 0)
1✔
297
                fltr.UpdatedBefore = &ubUnix
1✔
298
        }
299
        return nil
1✔
300
}
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