This repository has been archived by the owner on Jan 27, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathaccounts.go
424 lines (358 loc) · 14 KB
/
accounts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
package service
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"time"
"github.com/CiscoM31/godata"
"github.com/blevesearch/bleve"
"github.com/gofrs/uuid"
"github.com/golang/protobuf/ptypes/empty"
merrors "github.com/micro/go-micro/v2/errors"
"github.com/owncloud/ocis-accounts/pkg/proto/v0"
"github.com/owncloud/ocis-accounts/pkg/provider"
"github.com/tredoe/osutil/user/crypt"
"google.golang.org/protobuf/types/known/timestamppb"
// register crypt functions
_ "github.com/tredoe/osutil/user/crypt/apr1_crypt"
_ "github.com/tredoe/osutil/user/crypt/md5_crypt"
_ "github.com/tredoe/osutil/user/crypt/sha256_crypt"
_ "github.com/tredoe/osutil/user/crypt/sha512_crypt"
)
func (s Service) indexAccounts(path string) (err error) {
var f *os.File
if f, err = os.Open(path); err != nil {
s.log.Error().Err(err).Str("dir", path).Msg("could not open accounts folder")
return
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
s.log.Error().Err(err).Str("dir", path).Msg("could not list accounts folder")
return
}
for _, file := range list {
a := &proto.BleveAccount{
BleveType: "account",
}
if err = s.loadAccount(file.Name(), &a.Account); err != nil {
s.log.Error().Err(err).Str("account", file.Name()).Msg("could not load account")
continue
}
s.log.Debug().Interface("account", a).Msg("found account")
if err = s.index.Index(a.Id, a); err != nil {
s.log.Error().Err(err).Interface("account", a).Msg("could not index account")
continue
}
}
return
}
// an auth request is currently hardcoded and has to match this regex
// login eq \"teddy\" and password eq \"F&1!b90t111!\"
var authQuery = regexp.MustCompile(`^login eq '(.*)' and password eq '(.*)'$`) // TODO how is ' escaped in the password?
func (s Service) loadAccount(id string, a *proto.Account) (err error) {
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", id)
var data []byte
if data, err = ioutil.ReadFile(path); err != nil {
return merrors.NotFound(s.id, "could not read account: %v", err.Error())
}
if err = json.Unmarshal(data, a); err != nil {
return merrors.InternalServerError(s.id, "could not unmarshal account: %v", err.Error())
}
return
}
// loggableAccount redacts the password from the account
func loggableAccount(a *proto.Account) *proto.Account {
if a != nil && a.PasswordProfile != nil {
a.PasswordProfile.Password = "***REMOVED***"
}
return a
}
func (s Service) writeAccount(a *proto.Account) (err error) {
// leave only the group id
s.deflateMemberOf(a)
var bytes []byte
if bytes, err = json.Marshal(a); err != nil {
return merrors.InternalServerError(s.id, "could not marshal account: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", a.Id)
if err = ioutil.WriteFile(path, bytes, 0600); err != nil {
return merrors.InternalServerError(s.id, "could not write account: %v", err.Error())
}
return
}
func (s Service) expandMemberOf(a *proto.Account) {
if a == nil {
return
}
expanded := []*proto.Group{}
for i := range a.MemberOf {
g := &proto.Group{}
// TODO resolve by name, when a create or update is issued they may not have an id? fall back to searching the group id in the index?
if err := s.loadGroup(a.MemberOf[i].Id, g); err == nil {
g.Members = nil // always hide members when expanding
expanded = append(expanded, g)
} else {
// log errors but continue execution for now
s.log.Error().Err(err).Str("id", a.MemberOf[i].Id).Msg("could not load group")
}
}
a.MemberOf = expanded
}
// deflateMemberOf replaces the groups of a user with an instance that only contains the id
func (s Service) deflateMemberOf(a *proto.Account) {
if a == nil {
return
}
deflated := []*proto.Group{}
for i := range a.MemberOf {
if a.MemberOf[i].Id != "" {
deflated = append(deflated, &proto.Group{Id: a.MemberOf[i].Id})
} else {
// TODO fetch and use an id when group only has a name but no id
s.log.Error().Str("id", a.Id).Interface("group", a.MemberOf[i]).Msg("resolving groups by name is not implemented yet")
}
}
a.MemberOf = deflated
}
func (s Service) passwordIsValid(hash string, pwd string) (ok bool) {
defer func() {
if r := recover(); r != nil {
s.log.Error().Err(fmt.Errorf("%s", r)).Str("hash", hash).Msg("password lib panicked")
}
}()
c := crypt.NewFromHash(hash)
return c.Verify(hash, []byte(pwd)) == nil
}
// ListAccounts implements the AccountsServiceHandler interface
// the query contains account properties
func (s Service) ListAccounts(ctx context.Context, in *proto.ListAccountsRequest, out *proto.ListAccountsResponse) (err error) {
var password string
// check if this looks like an auth request
match := authQuery.FindStringSubmatch(in.Query)
if len(match) == 3 {
in.Query = fmt.Sprintf("on_premises_sam_account_name eq '%s'", match[1]) // todo fetch email? make query configurable
password = match[2]
if password == "" {
return merrors.Unauthorized(s.id, "password must not be empty")
}
}
// only search for accounts
tq := bleve.NewTermQuery("account")
tq.SetField("bleve_type")
query := bleve.NewConjunctionQuery(tq)
if in.Query != "" {
// parse the query like an odata filter
var q *godata.GoDataFilterQuery
if q, err = godata.ParseFilterString(in.Query); err != nil {
s.log.Error().Err(err).Msg("could not parse query")
return merrors.InternalServerError(s.id, "could not parse query: %v", err.Error())
}
// convert to bleve query
bq, err := provider.BuildBleveQuery(q)
if err != nil {
s.log.Error().Err(err).Msg("could not build bleve query")
return merrors.InternalServerError(s.id, "could not build bleve query: %v", err.Error())
}
query.AddQuery(bq)
}
s.log.Debug().Interface("query", query).Msg("using query")
searchRequest := bleve.NewSearchRequest(query)
var searchResult *bleve.SearchResult
searchResult, err = s.index.Search(searchRequest)
if err != nil {
s.log.Error().Err(err).Msg("could not execute bleve search")
return merrors.InternalServerError(s.id, "could not execute bleve search: %v", err.Error())
}
s.log.Debug().Interface("result", searchResult).Msg("result")
out.Accounts = make([]*proto.Account, 0)
for _, hit := range searchResult.Hits {
a := &proto.Account{}
if err = s.loadAccount(hit.ID, a); err != nil {
s.log.Error().Err(err).Str("account", hit.ID).Msg("could not load account, skipping")
continue
}
var currentHash string
if a.PasswordProfile != nil {
currentHash = a.PasswordProfile.Password
}
s.log.Debug().Interface("account", loggableAccount(a)).Msg("found account")
if password != "" {
if a.PasswordProfile == nil {
s.log.Debug().Interface("account", loggableAccount(a)).Msg("no password profile")
return merrors.Unauthorized(s.id, "invalid password")
}
if !s.passwordIsValid(currentHash, password) {
return merrors.Unauthorized(s.id, "invalid password")
}
}
// TODO add groups if requested
// if in.FieldMask ...
s.expandMemberOf(a)
// remove password before returning
a.PasswordProfile.Password = ""
out.Accounts = append(out.Accounts, a)
}
return
}
// GetAccount implements the AccountsServiceHandler interface
func (s Service) GetAccount(c context.Context, in *proto.GetAccountRequest, out *proto.Account) (err error) {
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
if err = s.loadAccount(id, out); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return
}
s.log.Debug().Interface("account", loggableAccount(out)).Msg("found account")
// TODO add groups if requested
// if in.FieldMask ...
s.expandMemberOf(out)
// remove password
out.PasswordProfile.Password = ""
return
}
// CreateAccount implements the AccountsServiceHandler interface
func (s Service) CreateAccount(c context.Context, in *proto.CreateAccountRequest, out *proto.Account) (err error) {
var id string
if in.Account == nil {
return merrors.BadRequest(s.id, "account missing")
}
if in.Account.Id == "" {
in.Account.Id = uuid.Must(uuid.NewV4()).String()
}
if id, err = cleanupID(in.Account.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", id)
if in.Account.PasswordProfile != nil && in.Account.PasswordProfile.Password != "" {
// encrypt password
c := crypt.New(crypt.SHA512)
if in.Account.PasswordProfile.Password, err = c.Generate([]byte(in.Account.PasswordProfile.Password), nil); err != nil {
s.log.Error().Err(err).Str("id", id).Interface("account", loggableAccount(in.Account)).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
}
}
// extract group id
// TODO groups should be ignored during create, use groups.AddMember? return error?
if err = s.writeAccount(in.Account); err != nil {
s.log.Error().Err(err).Interface("account", loggableAccount(in.Account)).Msg("could not persist new account")
return
}
if err = s.index.Index(id, in.Account); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Interface("account", loggableAccount(in.Account)).Msg("could not index new account")
return merrors.InternalServerError(s.id, "could not index new account: %v", err.Error())
}
return
}
// UpdateAccount implements the AccountsServiceHandler interface
// read only fields are ignored
// TODO how can we unset specific values? using the update mask
func (s Service) UpdateAccount(c context.Context, in *proto.UpdateAccountRequest, out *proto.Account) (err error) {
var id string
if in.Account == nil {
return merrors.BadRequest(s.id, "account missing")
}
if in.Account.Id == "" {
return merrors.BadRequest(s.id, "account id missing")
}
if id, err = cleanupID(in.Account.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", id)
if err = s.loadAccount(id, out); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return
}
s.log.Debug().Interface("account", loggableAccount(out)).Msg("found account")
t := time.Now()
tsnow := ×tamppb.Timestamp{
Seconds: t.Unix(),
Nanos: int32(t.Nanosecond()),
}
// id read-only
out.AccountEnabled = in.Account.AccountEnabled
out.IsResourceAccount = in.Account.IsResourceAccount
// creation-type read only
out.Identities = in.Account.Identities
out.DisplayName = in.Account.DisplayName
out.PreferredName = in.Account.PreferredName
out.UidNumber = in.Account.UidNumber
out.GidNumber = in.Account.GidNumber
out.Mail = in.Account.Mail // read only?
out.Description = in.Account.Description
if in.Account.PasswordProfile != nil && in.Account.PasswordProfile.Password != "" {
// encrypt password
c := crypt.New(crypt.SHA512)
if out.PasswordProfile.Password, err = c.Generate([]byte(in.Account.PasswordProfile.Password), nil); err != nil {
s.log.Error().Err(err).Str("id", id).Interface("account", loggableAccount(in.Account)).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
}
out.PasswordProfile.LastPasswordChangeDateTime = tsnow
}
// lastPasswordChangeDateTime calculated, see password
out.PasswordProfile.PasswordPolicies = in.Account.PasswordProfile.PasswordPolicies
out.PasswordProfile.ForceChangePasswordNextSignIn = in.Account.PasswordProfile.ForceChangePasswordNextSignIn
out.PasswordProfile.ForceChangePasswordNextSignInWithMfa = in.Account.PasswordProfile.ForceChangePasswordNextSignInWithMfa
// memberOf read only
// createdDateTime read only
// deleteDateTime read only
out.OnPremisesSyncEnabled = in.Account.OnPremisesSyncEnabled
out.OnPremisesSamAccountName = in.Account.OnPremisesSamAccountName
// ... TODO on prem for sync
if out.ExternalUserState != in.Account.ExternalUserState {
out.ExternalUserState = in.Account.ExternalUserState
out.ExternalUserStateChangeDateTime = tsnow
}
// out.RefreshTokensValidFromDateTime TODO use to invalidate all existing sessions
// out.SignInSessionsValidFromDateTime TODO use to invalidate all existing sessions
if err = s.writeAccount(out); err != nil {
s.log.Error().Err(err).Interface("account", loggableAccount(out)).Msg("could not persist updated account")
return
}
if err = s.index.Index(id, out); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Interface("account", loggableAccount(out)).Msg("could not index new account")
return merrors.InternalServerError(s.id, "could not index updated account: %v", err.Error())
}
// remove password
out.PasswordProfile.Password = ""
return
}
// DeleteAccount implements the AccountsServiceHandler interface
func (s Service) DeleteAccount(c context.Context, in *proto.DeleteAccountRequest, out *empty.Empty) (err error) {
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "accounts", id)
a := &proto.Account{}
if err = s.loadAccount(id, a); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return
}
// delete member relationship in groups
for i := range a.MemberOf {
err = s.RemoveMember(c, &proto.RemoveMemberRequest{
GroupId: a.MemberOf[i].Id,
AccountId: id,
}, a.MemberOf[i])
if err != nil {
s.log.Error().Err(err).Str("accountid", id).Str("groupid", a.MemberOf[i].Id).Msg("could not remove group member, skipping")
}
}
if err = os.Remove(path); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Msg("could not remove account")
return merrors.InternalServerError(s.id, "could not remove account: %v", err.Error())
}
if err = s.index.Delete(id); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Msg("could not remove account from index")
return merrors.InternalServerError(s.id, "could not remove account from index: %v", err.Error())
}
s.log.Info().Str("id", id).Msg("deleted account")
return
}