aboutsummaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.go115
-rw-r--r--app/models/session_key.go202
-rw-r--r--app/models/user.go99
3 files changed, 416 insertions, 0 deletions
diff --git a/app/models/account.go b/app/models/account.go
new file mode 100644
index 0000000..0ae1821
--- /dev/null
+++ b/app/models/account.go
@@ -0,0 +1,115 @@
1package models
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "code.crute.us/mcrute/golib/db/mongodb"
9)
10
11const accountCol = "accounts"
12
13type AccountStore interface {
14 List(context.Context) ([]*Account, error)
15 ListForUser(context.Context, *User) ([]*Account, error)
16 Get(context.Context, string) (*Account, error) // Error on not found
17 GetForUser(context.Context, string, *User) (*Account, error) // Error on not found
18 Put(context.Context, *Account) error
19 Delete(context.Context, *Account) error
20}
21
22type Account struct {
23 ShortName string `bson:"_id"`
24 AccountType string
25 AccountNumber int
26 Name string
27 ConsoleSessionDuration time.Duration
28 VaultMaterial string
29 DefaultRegion string
30 Users []string
31}
32
33func (a *Account) ConsoleSessionDurationSecs() int64 {
34 return int64(a.ConsoleSessionDuration.Seconds())
35}
36
37func (a *Account) CanAccess(u *User) bool {
38 if u.IsAdmin {
39 return true
40 }
41 // Linear search should be fine for now, these lists are pretty small
42 for _, n := range a.Users {
43 if n == u.Username {
44 return true
45 }
46 }
47 return false
48}
49
50type MongoDbAccountStore struct {
51 Db *mongodb.Mongo
52}
53
54// List returns all accounts in the system.
55func (s *MongoDbAccountStore) List(ctx context.Context) ([]*Account, error) {
56 var out []*Account
57 if err := s.Db.FindAll(ctx, accountCol, &out); err != nil {
58 return nil, err
59 }
60 return out, nil
61}
62
63// ListForUser returns all accounts for which the user has access. This is the
64// authorized version of List.
65//
66// Note this does not handle the case where a user is an admin but not
67// explicitly listed in the allowed users list for an account. For that case
68// just use List directly.
69func (s *MongoDbAccountStore) ListForUser(ctx context.Context, u *User) ([]*Account, error) {
70 var out []*Account
71 filter := mongodb.AnyInTopLevelArray("Users", u.Username)
72 if err := s.Db.FindAllByFilter(ctx, accountCol, filter, &out); err != nil {
73 return nil, err
74 }
75 return out, nil
76}
77
78func (s *MongoDbAccountStore) Get(ctx context.Context, id string) (*Account, error) {
79 var a Account
80 if err := s.Db.FindOneById(ctx, accountCol, id, &a); err != nil {
81 return nil, err
82 }
83 return &a, nil
84}
85
86// GetForUser returns an account if the user has access to this account,
87// otherwise it returns an error. This is the authorized version of Get.
88func (s *MongoDbAccountStore) GetForUser(ctx context.Context, id string, u *User) (*Account, error) {
89 a, err := s.Get(ctx, id)
90 if err != nil {
91 return nil, err
92 }
93
94 if !a.CanAccess(u) {
95 return nil, fmt.Errorf("User does not have access to account")
96 }
97
98 return a, nil
99}
100
101func (s *MongoDbAccountStore) Put(ctx context.Context, a *Account) error {
102 if err := s.Db.ReplaceOneById(ctx, accountCol, a.ShortName, a); err != nil {
103 return err
104 }
105 return nil
106}
107
108func (s *MongoDbAccountStore) Delete(ctx context.Context, a *Account) error {
109 if err := s.Db.DeleteOneById(ctx, accountCol, a.ShortName); err != nil {
110 return err
111 }
112 return nil
113}
114
115var _ AccountStore = (*MongoDbAccountStore)(nil)
diff --git a/app/models/session_key.go b/app/models/session_key.go
new file mode 100644
index 0000000..64ac7e0
--- /dev/null
+++ b/app/models/session_key.go
@@ -0,0 +1,202 @@
1package models
2
3import (
4 "crypto"
5 "crypto/ecdsa"
6 "crypto/elliptic"
7 "crypto/rand"
8 "crypto/x509"
9 "encoding/base64"
10 "encoding/hex"
11 "fmt"
12 "time"
13
14 "go.mongodb.org/mongo-driver/bson"
15)
16
17// SessionKey represents a public and sometimes private key-pair for a user
18// that will be stored on the user's record in the user store. These keys are
19// used for signing authentication JWTs.
20//
21// This object is designed to be serialized to BSON. Other serializations can
22// be added in the future as needed.
23//
24// There are two flavors of this record. A record with a private key (which
25// implies a public key) is a key that the service generated and is used by the
26// service to sign JWTs for the user. The private key is never given to the
27// user. The private key is only used in the CreateToken flow, never the Verify
28// flow. Currently (as of Nov 2021) the application sets a near-future NotAfter
29// date and these get garbage collected. It might be nice to re-use them in the
30// future for a while but it's not all that important.
31//
32// The other flavor of this key will have a public key but no private key.
33// These are service keys. Service keys are given to programmatic actors that
34// need to be able to mint their own JWTs for authentication to the service.
35// For these keys the client will construct their own JWT and sign it with the
36// private key and the service will validate the signature with the public key.
37// These keys (as of Nov 2021) do not expire, though they can be revoked.
38type SessionKey struct {
39 KeyId string
40 Description string
41 Revoked *time.Time
42 NotAfter *time.Time
43 NotBefore *time.Time
44 PublicKey crypto.PublicKey
45 PrivateKey *ecdsa.PrivateKey
46}
47
48func GenerateSessionKey(ttl time.Duration) (*SessionKey, error) {
49 pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
50 if err != nil {
51 return nil, err
52 }
53
54 key := make([]byte, 8)
55 if _, err := rand.Read(key); err != nil {
56 return nil, err
57 }
58
59 now := time.Now()
60 notAfter := now.Add(ttl)
61
62 return &SessionKey{
63 KeyId: hex.EncodeToString(key),
64 Revoked: nil,
65 NotAfter: &notAfter,
66 NotBefore: &now,
67 PublicKey: pk.Public(),
68 PrivateKey: pk,
69 }, nil
70}
71
72// IsGarbage checks to determine if a key is garbage that should be collected.
73// The definition of garbage is similar to the inversion of the definition of
74// vaild but revoked keys are not considered to be garbage since they may be
75// useful for auditing later. Also keys that are not yet valid are not garbage.
76func (s *SessionKey) IsGarbage() bool {
77 if s.Revoked != nil {
78 return false
79 }
80
81 if s.NotBefore != nil && s.NotBefore.Before(time.Now()) {
82 return false
83 }
84
85 if s.NotAfter != nil && s.NotAfter.After(time.Now()) {
86 return true
87 }
88
89 return false
90}
91
92// IsValid checks the various dates in the SessionKey to verify that they are
93// valid and in-range for use. This should be called before trusting this key
94// for any use.
95func (s *SessionKey) IsValid() bool {
96 if s.Revoked != nil {
97 return false
98 }
99
100 if s.NotBefore != nil && s.NotBefore.Before(time.Now()) {
101 return false
102 }
103
104 if s.NotAfter != nil && s.NotAfter.After(time.Now()) {
105 return false
106 }
107
108 return true
109}
110
111func (s *SessionKey) MarshalBSON() ([]byte, error) {
112 var err error
113 var pub, priv []byte
114
115 if s.PrivateKey != nil {
116 priv, err = x509.MarshalECPrivateKey(s.PrivateKey)
117 if err != nil {
118 return nil, err
119 }
120 }
121
122 // If there's a private key and a public key set then just save the private
123 // key. The private key already contains a copy of the public key.
124 if s.PublicKey != nil && s.PrivateKey == nil {
125 pub, err = x509.MarshalPKIXPublicKey(s.PublicKey)
126 if err != nil {
127 return nil, err
128 }
129 }
130
131 return bson.Marshal(struct {
132 KeyId string
133 Revoked *time.Time
134 NotAfter *time.Time
135 NotBefore *time.Time
136 PublicKey string
137 PrivateKey string
138 }{
139 s.KeyId,
140 s.Revoked, s.NotAfter, s.NotBefore,
141 base64.StdEncoding.EncodeToString(pub),
142 base64.StdEncoding.EncodeToString(priv),
143 })
144}
145
146func (s *SessionKey) UnmarshalBSON(d []byte) error {
147 v := struct {
148 KeyId string
149 Revoked *time.Time
150 NotAfter *time.Time
151 NotBefore *time.Time
152 PublicKey string
153 PrivateKey string
154 }{}
155 if err := bson.Unmarshal(d, &v); err != nil {
156 return err
157 }
158
159 s.KeyId = v.KeyId
160 s.Revoked = v.Revoked
161 s.NotAfter = v.NotAfter
162 s.NotBefore = v.NotBefore
163
164 if v.PrivateKey != "" {
165 privb, err := base64.StdEncoding.DecodeString(v.PrivateKey)
166 if err != nil {
167 return err
168 }
169
170 priv, err := x509.ParseECPrivateKey(privb)
171 if err != nil {
172 return err
173 }
174
175 s.PrivateKey = priv
176 s.PublicKey = priv.Public()
177 }
178
179 // If there was a private key then the public key was already set by
180 // decoding that private key. No need to do this a second time (also it's
181 // rather unlikely that both would be set).
182 if v.PublicKey != "" && s.PublicKey == nil {
183 pubb, err := base64.StdEncoding.DecodeString(v.PublicKey)
184 if err != nil {
185 return err
186 }
187
188 pubp, err := x509.ParsePKIXPublicKey(pubb)
189 if err != nil {
190 return err
191 }
192
193 pub, ok := pubp.(*ecdsa.PublicKey)
194 if !ok {
195 return fmt.Errorf("Failed to convert public key to *ecdsa.PublicKey")
196 }
197
198 s.PublicKey = pub
199 }
200
201 return nil
202}
diff --git a/app/models/user.go b/app/models/user.go
new file mode 100644
index 0000000..0cbd92d
--- /dev/null
+++ b/app/models/user.go
@@ -0,0 +1,99 @@
1package models
2
3import (
4 "context"
5
6 "code.crute.us/mcrute/golib/db/mongodb"
7)
8
9const userCol = "users"
10
11type UserStore interface {
12 List(context.Context) ([]*User, error)
13 Get(context.Context, string) (*User, error) // Error on not found
14 Put(context.Context, *User) error
15 Delete(context.Context, *User) error
16}
17
18type AuthToken struct {
19 Kind string
20 Token string
21 RefreshToken string
22}
23
24type User struct {
25 Username string `bson:"_id"`
26 IsAdmin bool
27 IsService bool
28 Keys map[string]*SessionKey // kid -> key
29 AuthTokens map[string]*AuthToken // kind -> token
30}
31
32// GCKeys garbage collects keys that are no longer valid
33func (u *User) GCKeys() {
34 for k, v := range u.Keys {
35 if v.IsGarbage() {
36 delete(u.Keys, k)
37 }
38 }
39}
40
41// GetKey returns a key for a key ID. It will only return valid keys.
42func (u *User) GetKey(kid string) *SessionKey {
43 if u.Keys != nil {
44 if k := u.Keys[kid]; k != nil && k.IsValid() {
45 return k
46 }
47 }
48 return nil
49}
50
51func (u *User) AddKey(k *SessionKey) {
52 if u.Keys == nil {
53 u.Keys = map[string]*SessionKey{}
54 }
55 u.Keys[k.KeyId] = k
56}
57
58func (u *User) AddToken(t *AuthToken) {
59 if u.AuthTokens == nil {
60 u.AuthTokens = map[string]*AuthToken{}
61 }
62 u.AuthTokens[t.Kind] = t
63}
64
65type MongoDbUserStore struct {
66 Db *mongodb.Mongo
67}
68
69func (s *MongoDbUserStore) List(ctx context.Context) ([]*User, error) {
70 var out []*User
71 if err := s.Db.FindAll(ctx, userCol, &out); err != nil {
72 return nil, err
73 }
74 return out, nil
75}
76
77func (s *MongoDbUserStore) Get(ctx context.Context, username string) (*User, error) {
78 var u User
79 if err := s.Db.FindOneById(ctx, userCol, username, &u); err != nil {
80 return nil, err
81 }
82 return &u, nil
83}
84
85func (s *MongoDbUserStore) Put(ctx context.Context, u *User) error {
86 if err := s.Db.ReplaceOneById(ctx, userCol, u.Username, u); err != nil {
87 return err
88 }
89 return nil
90}
91
92func (s *MongoDbUserStore) Delete(ctx context.Context, u *User) error {
93 if err := s.Db.DeleteOneById(ctx, userCol, u.Username); err != nil {
94 return err
95 }
96 return nil
97}
98
99var _ UserStore = (*MongoDbUserStore)(nil)