-
Notifications
You must be signed in to change notification settings - Fork 397
/
service.go
175 lines (144 loc) · 4.98 KB
/
service.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
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package restkeys
import (
"context"
"crypto/sha256"
"database/sql"
"errors"
"time"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/oidc"
)
var mon = monkit.Package()
var (
// Error describes internal rest keys error.
Error = errs.Class("rest keys service")
// ErrDuplicateKey is error type that occurs when a generated account
// management api key already exists.
ErrDuplicateKey = errs.Class("duplicate key")
// ErrInvalidKey is an error type that occurs when a user submits a key
// that does not match anything in the database.
ErrInvalidKey = errs.Class("invalid key")
)
// Config contains configuration parameters for rest keys.
type Config struct {
DefaultExpiration time.Duration `help:"expiration to use if user does not specify an rest key expiration" default:"720h"`
}
// Service handles operations regarding rest keys.
type Service struct {
db oidc.OAuthTokens
config Config
}
// NewService creates a new rest keys service.
func NewService(db oidc.OAuthTokens, config Config) *Service {
return &Service{
db: db,
config: config,
}
}
// Create creates and inserts an rest key into the db.
func (s *Service) Create(ctx context.Context, userID uuid.UUID, expiration time.Duration) (apiKey string, expiresAt time.Time, err error) {
defer mon.Task()(&ctx)(&err)
apiKey, hash, err := s.GenerateNewKey(ctx)
if err != nil {
return "", time.Time{}, Error.Wrap(err)
}
expiresAt, err = s.InsertIntoDB(ctx, oidc.OAuthToken{
UserID: userID,
Kind: oidc.KindRESTTokenV0,
Token: hash,
}, time.Now(), expiration)
if err != nil {
return "", time.Time{}, Error.Wrap(err)
}
return apiKey, expiresAt, nil
}
// GenerateNewKey generates a new account management api key.
func (s *Service) GenerateNewKey(ctx context.Context) (apiKey, hash string, err error) {
defer mon.Task()(&ctx)(&err)
id, err := uuid.New()
if err != nil {
return "", "", Error.Wrap(err)
}
apiKey = id.String()
hash = hashKeyFromUUID(ctx, id)
return apiKey, hash, nil
}
// This is used for hashing during key creation so we don't need to convert from a string back to a uuid.
func hashKeyFromUUID(ctx context.Context, apiKeyUUID uuid.UUID) string {
mon.Task()(&ctx)(nil)
hashBytes := sha256.Sum256(apiKeyUUID.Bytes())
return string(hashBytes[:])
}
// HashKey returns a hash of api key. This is used for hashing inside GetUserFromKey.
func (s *Service) HashKey(ctx context.Context, apiKey string) (hash string, err error) {
defer mon.Task()(&ctx)(&err)
id, err := uuid.FromString(apiKey)
if err != nil {
return "", Error.Wrap(err)
}
hashBytes := sha256.Sum256(id.Bytes())
return string(hashBytes[:]), nil
}
// InsertIntoDB checks OAuthTokens DB for a token before inserting. This is because OAuthTokens DB allows
// duplicate tokens, but we can't have duplicate api keys.
func (s *Service) InsertIntoDB(ctx context.Context, oAuthToken oidc.OAuthToken, now time.Time, expiration time.Duration) (expiresAt time.Time, err error) {
defer mon.Task()(&ctx)(&err)
// The token column is the key to the OAuthTokens table, but the Create method does not return an error if a duplicate token insert is attempted.
// We need to make sure a unique api key is created, so check that the value doesn't already exist.
_, err = s.db.Get(ctx, oidc.KindRESTTokenV0, oAuthToken.Token)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return time.Time{}, Error.Wrap(err)
}
} else if err == nil {
return time.Time{}, Error.Wrap(ErrDuplicateKey.New("failed to generate a unique account management api key"))
}
if expiration <= 0 {
expiration = s.config.DefaultExpiration
}
expiresAt = now.Add(expiration)
oAuthToken.CreatedAt = now
oAuthToken.ExpiresAt = expiresAt
err = s.db.Create(ctx, oAuthToken)
if err != nil {
return time.Time{}, Error.Wrap(err)
}
return expiresAt, nil
}
// GetUserAndExpirationFromKey gets the userID and expiration date attached to an account management api key.
func (s *Service) GetUserAndExpirationFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, exp time.Time, err error) {
defer mon.Task()(&ctx)(&err)
hash, err := s.HashKey(ctx, apiKey)
if err != nil {
return uuid.UUID{}, time.Now(), err
}
keyInfo, err := s.db.Get(ctx, oidc.KindRESTTokenV0, hash)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return uuid.UUID{}, time.Now(), Error.Wrap(ErrInvalidKey.New("invalid account management api key"))
}
return uuid.UUID{}, time.Now(), err
}
return keyInfo.UserID, keyInfo.ExpiresAt, err
}
// Revoke revokes an account management api key.
func (s *Service) Revoke(ctx context.Context, apiKey string) (err error) {
defer mon.Task()(&ctx)(&err)
hash, err := s.HashKey(ctx, apiKey)
if err != nil {
return Error.Wrap(err)
}
_, err = s.db.Get(ctx, oidc.KindRESTTokenV0, hash)
if err != nil {
return Error.Wrap(err)
}
err = s.db.RevokeRESTTokenV0(ctx, hash)
if err != nil {
return Error.Wrap(err)
}
return nil
}