1 package database
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "net/url"
9 "strings"
10 "time"
11
12 "edge-infra.dev/pkg/edge/iam/config"
13 "edge-infra.dev/pkg/edge/iam/crypto"
14 iamErrors "edge-infra.dev/pkg/edge/iam/errors"
15 "edge-infra.dev/pkg/edge/iam/log"
16 "edge-infra.dev/pkg/edge/iam/storage"
17
18 "github.com/go-kivik/kivik/v4"
19 "github.com/go-kivik/kivik/v4/couchdb"
20 "github.com/go-logr/logr"
21 "github.com/go-redis/redis"
22 "github.com/ory/fosite"
23 "github.com/pkg/errors"
24 )
25
26 type (
27 KeyPrefix string
28 Store struct {
29 Log logr.Logger
30 CouchDB *kivik.Client
31 RedisDB *redis.Client
32 Sessions *SessionStore
33 CouchDBLocal *kivik.Client
34 isOffline bool
35 }
36 Doc struct {
37 ID string `json:"_id"`
38 Value json.RawMessage `json:"value"`
39 Rev string `json:"_rev,omitempty"`
40 Expiration int64 `json:"expiration,omitempty"`
41 }
42 Options func(d *Doc)
43 )
44
45 const (
46 KeyPrefixAuthorizationCode KeyPrefix = "auth-code"
47 KeyPrefixAccessToken KeyPrefix = "access-token"
48 KeyPrefixAccessTokenReq KeyPrefix = "access-token-request"
49 KeyPrefixRefreshToken KeyPrefix = "refresh-token"
50 KeyPrefixRefreshTokenReq KeyPrefix = "refresh-token-request"
51 KeyPrefixOpenIDConnect KeyPrefix = "oidc"
52 KeyPrefixPKCE KeyPrefix = "pkce"
53 KeyPrefixClientCreds KeyPrefix = "client-creds"
54 KeyPrefixClientProfile KeyPrefix = "client-profile"
55 KeyPrefixClient KeyPrefix = "client"
56 KeyPrefixPIN KeyPrefix = "pin"
57 KeyPrefixDeviceAccount KeyPrefix = "device-acct"
58 KeyPrefixProfile KeyPrefix = "profile"
59 KeyPrefixAlias KeyPrefix = "alias"
60 KeyPrefixDeviceLogin KeyPrefix = "device-login"
61 KeyPrefixBarcode KeyPrefix = "barcode"
62 KeyPrefixBarcodeCode KeyPrefix = "barcode-code"
63 KeyPrefixBarcodeKey KeyPrefix = "barcode-key"
64 KeyPrefixBarcodeUser KeyPrefix = "barcode-user"
65 KeyPrefixLoginHint KeyPrefix = "login-hint"
66 )
67
68 const AccountsDBName = "iam-accounts"
69
70
71 func WithExpiration(ttl time.Duration) Options {
72 return func(d *Doc) {
73 expiration := time.Now().Unix() + int64(ttl.Seconds())
74 d.Expiration = expiration
75 }
76 }
77
78
79 func NewOperatorStore(log logr.Logger) (*Store, error) {
80 couchdb, err := NewCouchDBClient(log)
81 if err != nil {
82 return nil, err
83 }
84 return &Store{
85 CouchDB: couchdb,
86 Log: log,
87 }, err
88 }
89
90
91 func NewProviderStore(log logr.Logger) (*Store, error) {
92 couchdb, err := NewCouchDBClient(log)
93 if err != nil {
94 return nil, err
95 }
96 redis, err := NewRedisClient()
97 if err != nil {
98 return nil, err
99 }
100 sessionStore, err := NewRedisSessionStore(context.Background(), redis)
101 if err != nil {
102 return nil, err
103 }
104 store := &Store{
105 RedisDB: redis,
106 Sessions: sessionStore,
107 CouchDB: couchdb,
108 Log: log,
109 }
110 if config.IsTouchpoint() {
111 var err error
112 store.CouchDBLocal, err = LocalCouchDBClient(log)
113 if err != nil {
114 return nil, err
115 }
116
117 go store.periodicDetection(time.Second * 60)
118 }
119 return store, nil
120 }
121
122 type redisSchema struct {
123 ID string `json:"id"`
124 RequestedAt time.Time `json:"requestedAt"`
125 ClientID string `json:"clientId"`
126 Scopes fosite.Arguments `json:"scopes"`
127 GrantedScopes fosite.Arguments `json:"grantedScopes"`
128 Form url.Values `json:"formData"`
129 Session json.RawMessage `json:"sessionData"`
130 Active bool `json:"active"`
131 }
132
133
134
135
136
137
138
139
140
141
142
143
144 func NewCouchDBClient(log logr.Logger) (*kivik.Client, error) {
145 couchURI := config.CouchDBAddress()
146 couchClient, err := kivik.New("couch", couchURI, couchdb.BasicAuth(config.CouchDBUser(), config.CouchDBPassword()))
147 if err != nil {
148 return nil, err
149 }
150
151
152 if config.IsTouchpoint() {
153 return couchClient, err
154 }
155 exists, err := couchClient.DBExists(context.Background(), AccountsDBName)
156 if err != nil {
157 return nil, err
158 }
159 if !exists {
160 if err := couchClient.CreateDB(context.Background(), AccountsDBName); err != nil {
161 return nil, err
162 }
163 }
164
165 row := couchClient.DB(AccountsDBName).Get(context.Background(), "repl_doc")
166 if row.Err() != nil {
167 if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
168 err := putReplDoc(couchClient.DB(AccountsDBName))
169 if err != nil {
170 log.Error(err, "failed to PUT repl_doc into database")
171 return nil, err
172 }
173 log.Info("successfully PUT repl_doc into database")
174 } else {
175 log.Error(row.Err(), "failed to retrieve repl_doc status")
176 return nil, err
177 }
178 } else {
179 log.Info("repl_doc exists, no action required")
180 }
181
182 log.Info("connected to store couchdb", "address", couchURI)
183 return couchClient, nil
184 }
185
186 func LocalCouchDBClient(log logr.Logger) (*kivik.Client, error) {
187 couchURI := config.CouchDBAddressLocal()
188
189 username, err := config.CouchDBUserLocal()
190 if err != nil {
191 return nil, err
192 }
193
194
195 password, err := config.CouchDBPasswordLocal()
196 if err != nil {
197 return nil, err
198 }
199 couchClient, err := kivik.New("couch", couchURI, couchdb.BasicAuth(username, password))
200 if err != nil {
201 return nil, err
202 }
203 log.Info("connected to touchpoint couchdb", "address", couchURI)
204 return couchClient, nil
205 }
206
207
208 func putReplDoc(db *kivik.DB) error {
209 datasets := []byte(`{
210 "datasets": [{
211 "config": {
212 "cancel": false,
213 "continuous": true,
214 "create_target": true,
215 "doc_ids": null,
216 "filter": "",
217 "interval": "60000",
218 "query_params": "",
219 "selector": "",
220 "since_seq": "",
221 "source_proxy": "",
222 "target_proxy": "",
223 "use_checkpoints": false
224 },
225 "name": "iam-accounts"
226 }]
227 }`)
228 _, err := db.Put(context.Background(), "repl_doc", datasets)
229 if err != nil {
230 return err
231 }
232 return nil
233 }
234
235 func NewRedisClient() (*redis.Client, error) {
236 logger := log.Logger()
237 redisAddress := config.RedisAddress()
238
239 db := redis.NewClient(&redis.Options{
240 Addr: redisAddress,
241 })
242
243 err := db.Ping().Err()
244 if err != nil {
245 return nil, err
246 }
247 logger.Info("connected to redis", "address", redisAddress)
248 return db, nil
249 }
250
251 func (s *Store) set(key string, request storage.Request, ttl time.Duration) error {
252 payload, err := json.Marshal(request)
253 if err != nil {
254 return errors.WithStack(err)
255 }
256
257 if config.EncryptionEnabled() {
258 encryptedValue, err := crypto.EncryptRedis(payload, config.EncryptionKey())
259 if err != nil {
260 return errors.WithStack(err)
261 }
262 if err := s.RedisDB.Set(key, encryptedValue, ttl).Err(); err != nil {
263 return errors.WithStack(err)
264 }
265 } else {
266 if err := s.RedisDB.Set(key, payload, ttl).Err(); err != nil {
267 return errors.WithStack(err)
268 }
269 }
270
271 return nil
272 }
273
274 func (s *Store) get(key string) (*storage.Request, error) {
275 resp, err := s.RedisDB.Get(key).Bytes()
276 if err != nil {
277 return nil, err
278 }
279
280 var schema redisSchema
281 if config.EncryptionEnabled() {
282
283 if !isRedisDataEncrypted(resp) {
284 ttl := s.RedisDB.TTL(key)
285 encryptedVal, err := crypto.EncryptRedis(resp, config.EncryptionKey())
286 if err != nil {
287 return nil, err
288 }
289
290 err = s.RedisDB.Set(key, encryptedVal, ttl.Val()).Err()
291 if err != nil {
292 return nil, err
293 }
294 } else {
295 decryptedValue, err := crypto.DecryptRedis(string(resp), config.EncryptionKey())
296 if err != nil {
297 return nil, err
298 }
299 resp = decryptedValue
300 }
301 }
302
303 if err := json.Unmarshal(resp, &schema); err != nil {
304 return nil, err
305 }
306
307 return &storage.Request{
308 ID: schema.ID,
309 RequestedAt: schema.RequestedAt,
310 ClientID: schema.ClientID,
311 RequestedScope: schema.Scopes,
312 GrantedScope: schema.GrantedScopes,
313 Form: schema.Form,
314 Session: schema.Session,
315 Active: schema.Active,
316 }, nil
317 }
318
319
320 func (s *Store) findDoc(ctx context.Context, query interface{}) ([]*Doc, error) {
321 rows := s.CouchDB.DB(AccountsDBName).Find(ctx, query)
322 var docs []*Doc
323 for rows.Next() {
324 var doc *Doc
325 if err := rows.ScanDoc(&doc); err != nil {
326 return nil, err
327 }
328 docs = append(docs, doc)
329 }
330 if rows.Err() != nil {
331 return nil, rows.Err()
332 }
333 return docs, nil
334 }
335
336 func (s *Store) getRow(ctx context.Context, docID string) *kivik.Document {
337 if s.CouchDBLocal != nil && s.IsOffline() {
338 return s.CouchDBLocal.DB(AccountsDBName).Get(ctx, docID)
339 }
340 return s.CouchDB.DB(AccountsDBName).Get(ctx, docID)
341 }
342
343
344 func (s *Store) getDoc(ctx context.Context, docID string) (*Doc, error) {
345
346
347
348 if s.CouchDBLocal != nil && !s.IsOffline() {
349 s.offlineDetection()
350 }
351 var row *kivik.Document
352 var doc *Doc
353
354 row = s.getRow(ctx, docID)
355
356 if row.Err() != nil {
357 if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
358 return nil, nil
359 }
360 s.Log.Error(row.Err(), "failed to retrieve doc: ")
361 return nil, row.Err()
362 }
363
364 if err := row.ScanDoc(&doc); err != nil {
365 return nil, err
366 }
367
368
369 if doc.Expiration < time.Now().Unix() && doc.Expiration != 0 {
370 if _, err := s.CouchDB.DB(AccountsDBName).Delete(ctx, docID, doc.Rev); err != nil {
371 return nil, err
372 }
373 return nil, nil
374 }
375
376 if config.EncryptionEnabled() && startsWithPrefix(docID) {
377 key := config.EncryptionKey()
378 msg := &map[string]string{}
379
380
381
382 err := json.Unmarshal(doc.Value, msg)
383 if err != nil {
384
385 if _, ok := err.(*json.UnmarshalTypeError); !ok {
386 return nil, err
387 }
388 }
389
390 encryptedData := (*msg)["EncryptedData"]
391 if encryptedData == "" {
392 encryptedValue, err := crypto.EncryptJSON(doc.Value, key)
393 if err != nil {
394 return nil, err
395 }
396
397 doc.Value = encryptedValue
398
399
400 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil {
401 return nil, err
402 }
403 }
404
405 value, err := crypto.DecryptJSON(doc.Value, key)
406 if err != nil {
407 return nil, err
408 }
409 doc.Value = value
410 }
411
412 return doc, nil
413 }
414
415
416 func (s *Store) updateDoc(ctx context.Context, docID string, value []byte, opts ...Options) error {
417 var doc *Doc
418 var err error
419 if doc, err = s.getDoc(ctx, docID); err != nil {
420 return err
421 }
422
423 if doc == nil {
424 doc = &Doc{}
425 }
426 doc.ID = docID
427
428 if config.EncryptionEnabled() && startsWithPrefix(docID) {
429 encryptedValue, err := crypto.EncryptJSON(value, config.EncryptionKey())
430 if err != nil {
431 return err
432 }
433
434 doc.Value = encryptedValue
435 } else {
436 doc.Value = value
437 }
438
439
440 for _, o := range opts {
441 o(doc)
442 }
443
444
445
446 if s.CouchDBLocal != nil && s.IsOffline() {
447 s.offlineDetection()
448
449 if s.IsOffline() {
450
451 return iamErrors.ErrOffline
452 }
453 }
454
455
456 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil {
457 return err
458 }
459
460 return nil
461 }
462
463
464
465 func (s *Store) copyDoc(ctx context.Context, docID string, value []byte, expiration int64) error {
466 var doc *Doc
467 var err error
468 if doc, err = s.getDoc(ctx, docID); err != nil {
469 return err
470 }
471
472 if doc == nil {
473 doc = &Doc{}
474 }
475 doc.ID = docID
476 doc.Value = value
477 doc.Expiration = expiration
478
479 if !s.IsOffline() {
480 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil {
481 return err
482 }
483 } else {
484 return iamErrors.ErrOffline
485 }
486
487 return nil
488 }
489
490
491 func (s *Store) createDoc(ctx context.Context, docID string, value []byte, opts ...Options) error {
492 var doc *Doc
493 if config.EncryptionEnabled() {
494 encryptedValue, err := crypto.EncryptJSON(value, config.EncryptionKey())
495 if err != nil {
496 return err
497 }
498 doc = &Doc{
499 ID: docID,
500 Value: encryptedValue,
501 }
502 } else {
503 doc = &Doc{
504 ID: docID,
505 Value: value,
506 }
507 }
508
509
510 for _, o := range opts {
511 o(doc)
512 }
513
514 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil &&
515 kivik.HTTPStatus(err) != http.StatusConflict {
516 return err
517 }
518
519 return nil
520 }
521
522 func (s *Store) deleteDoc(ctx context.Context, docID string) error {
523 var doc *Doc
524 var err error
525 if doc, err = s.getDoc(ctx, docID); err != nil {
526 return err
527 }
528
529 if doc == nil {
530
531 return nil
532 }
533 if _, err := s.CouchDB.DB(AccountsDBName).Delete(ctx, docID, doc.Rev); err != nil {
534 return err
535 }
536
537 return nil
538 }
539
540 func (s *Store) periodicDetection(interval time.Duration) {
541 for {
542 s.offlineDetection()
543 time.Sleep(interval)
544 }
545 }
546
547 func (s *Store) offlineDetection() {
548 pingCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
549 defer cancel()
550
551 online, err := s.CouchDB.Ping(pingCtx)
552 if err != nil && !s.IsOffline() {
553 s.Log.Error(err, "network outage detected, failing over to touchpoint")
554 }
555 if online && s.IsOffline() {
556 s.Log.Info("network outage recovered, switching back to store")
557 }
558 s.isOffline = !online
559 }
560
561 func (s *Store) IsOffline() bool {
562 return s.isOffline
563 }
564
565 func (s *Store) RunOfflineDetection() {
566 s.offlineDetection()
567 }
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630 func keyFrom(kp KeyPrefix, v string) string {
631 key := fmt.Sprintf("%v:%v", kp, v)
632 return key
633 }
634
635
636 func (s *Store) RotateCouchEncryptionKey(ctx context.Context, oldKey []byte, newKey []byte) error {
637 rows := s.CouchDB.DB(AccountsDBName).AllDocs(ctx)
638
639 for rows.Next() {
640 id, err := rows.ID()
641 if err != nil {
642 return err
643 }
644
645 if !startsWithPrefix(id) {
646 continue
647 }
648
649
650 doc, err := s.GetDocWithKey(ctx, id, oldKey)
651 if err != nil {
652 return err
653 }
654
655
656 value, err := crypto.EncryptJSON(doc.Value, newKey)
657 if err != nil {
658 return err
659 }
660
661 doc.Value = value
662
663
664 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, id, doc); err != nil {
665 return err
666 }
667 }
668
669 if rows.Err() != nil {
670 return rows.Err()
671 }
672
673 return nil
674 }
675
676
677
678
679 func (s *Store) GetDocWithKey(ctx context.Context, docID string, oldKey []byte) (*Doc, error) {
680 var row *kivik.Document
681 var doc *Doc
682
683 row = s.getRow(ctx, docID)
684
685 if row.Err() != nil {
686 if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
687 return nil, nil
688 }
689 s.Log.Error(row.Err(), "failed to retrieve doc: ")
690 return nil, row.Err()
691 }
692
693 if err := row.ScanDoc(&doc); err != nil {
694 return nil, err
695 }
696
697
698 if doc.Expiration < time.Now().Unix() && doc.Expiration != 0 {
699 if _, err := s.CouchDB.DB(AccountsDBName).Delete(ctx, docID, doc.Rev); err != nil {
700 return nil, err
701 }
702 return nil, nil
703 }
704
705 value, err := crypto.DecryptJSON(doc.Value, oldKey)
706 if err != nil {
707 return nil, err
708 }
709 doc.Value = value
710
711 return doc, nil
712 }
713
714 func (s *Store) EncryptCouchDB(ctx context.Context, key []byte) error {
715 var doc *Doc
716
717 rows := s.CouchDB.DB(AccountsDBName).AllDocs(ctx)
718
719 for rows.Next() {
720 id, err := rows.ID()
721 if err != nil {
722 return err
723 }
724
725 if !startsWithPrefix(id) {
726 continue
727 }
728
729 row := s.getRow(ctx, id)
730
731 if row.Err() != nil {
732 if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
733 return nil
734 }
735 s.Log.Error(row.Err(), "failed to retrieve doc: ")
736 return row.Err()
737 }
738
739 if err := row.ScanDoc(&doc); err != nil {
740 return err
741 }
742
743
744 encrypted, err := isEncrypted(doc.Value)
745 if err != nil {
746 return err
747 }
748
749 if !encrypted {
750 encryptedValue, err := crypto.EncryptJSON(doc.Value, key)
751 if err != nil {
752 return err
753 }
754
755 doc.Value = encryptedValue
756
757
758 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, id, doc); err != nil {
759 return err
760 }
761 }
762 }
763
764 if rows.Err() != nil {
765 return rows.Err()
766 }
767
768 return nil
769 }
770
771 func (s *Store) DecryptCouchDB(ctx context.Context, key []byte) error {
772 var doc *Doc
773
774 rows := s.CouchDB.DB(AccountsDBName).AllDocs(ctx)
775
776 for rows.Next() {
777 id, err := rows.ID()
778
779 if err != nil {
780 return err
781 }
782
783
784 if !startsWithPrefix(id) {
785 continue
786 }
787
788 row := s.getRow(ctx, id)
789
790 if row.Err() != nil {
791 if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
792 return nil
793 }
794 s.Log.Error(row.Err(), "failed to retrieve doc: ")
795 return row.Err()
796 }
797
798 if err := row.ScanDoc(&doc); err != nil {
799 return err
800 }
801
802
803 encrypted, err := isEncrypted(doc.Value)
804 if err != nil {
805 return err
806 }
807
808 if encrypted {
809 encryptedValue, err := crypto.DecryptJSON(doc.Value, key)
810 if err != nil {
811 return err
812 }
813
814 doc.Value = encryptedValue
815
816
817 if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, id, doc); err != nil {
818 return err
819 }
820 }
821 }
822
823 if rows.Err() != nil {
824 return rows.Err()
825 }
826
827 return nil
828 }
829
830
831
832
833
834
835 func isEncrypted(value json.RawMessage) (bool, error) {
836 msg := &map[string]string{}
837 err := json.Unmarshal(value, msg)
838 if err != nil {
839
840 if _, ok := err.(*json.UnmarshalTypeError); ok {
841 return false, nil
842 }
843 return true, err
844 }
845
846
847 encryptedData := (*msg)["EncryptedData"]
848
849
850 if encryptedData != "" {
851 return true, nil
852 }
853 return false, nil
854 }
855
856
857
858 func startsWithPrefix(id string) bool {
859 allowedPrefixes := []KeyPrefix{KeyPrefixAuthorizationCode, KeyPrefixAccessToken,
860 KeyPrefixAccessTokenReq, KeyPrefixRefreshToken, KeyPrefixRefreshTokenReq, KeyPrefixOpenIDConnect,
861 KeyPrefixPKCE, KeyPrefixPIN, KeyPrefixProfile, KeyPrefixAlias, KeyPrefixBarcode,
862 KeyPrefixBarcodeCode, KeyPrefixBarcodeKey, KeyPrefixBarcodeUser, KeyPrefixLoginHint}
863
864 for _, prefix := range allowedPrefixes {
865 if strings.HasPrefix(id, string(prefix)) {
866 return true
867 }
868 }
869 return false
870 }
871
View as plain text