This repository has been archived by the owner on Jul 12, 2023. It is now read-only.
/
secret.go
238 lines (200 loc) · 6.57 KB
/
secret.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
// Copyright 2021 the Exposure Notifications Verification Server authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"fmt"
"strings"
"time"
"github.com/google/exposure-notifications-verification-server/internal/project"
"github.com/jinzhu/gorm"
)
// SecretType represents a secret type.
type SecretType string
const (
SecretTypeAPIKeyDatabaseHMAC = SecretType("api_key_database_hmac")
SecretTypeAPIKeySignatureHMAC = SecretType("api_key_signature_hmac")
SecretTypeCookieKeys = SecretType("cookie_keys")
SecretTypePhoneNumberDatabaseHMAC = SecretType("phone_number_database_hmac")
SecretTypeVerificationCodeDatabaseHMAC = SecretType("verification_code_database_hmac")
)
var _ Auditable = (*Secret)(nil)
// Secret represents the reference to a secret in an upstream secret manager. It
// exists to facilitate rotation and auditing.
type Secret struct {
Errorable
// ID is the primary key of the secret.
ID uint
// Type is the type of secret.
Type SecretType
// Reference is the pointer to the secret in the secret manager.
Reference string
// Active is a boolean indicating whether this secret is active.
Active bool
// CreatedAt, UpdatedAt, and DeletedAt are the timestamps.
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
func (s *Secret) AuditID() string {
return fmt.Sprintf("secret:%d", s.ID)
}
func (s *Secret) AuditDisplay() string {
return fmt.Sprintf("%s (%s)", s.Type, s.Reference)
}
// BeforeSave runs validations. If there are errors, the save fails.
func (s *Secret) BeforeSave(tx *gorm.DB) error {
s.Reference = project.TrimSpace(s.Reference)
if s.Reference == "" {
s.AddError("reference", "cannot be blank")
}
if err := s.ErrorOrNil(); err != nil {
return fmt.Errorf("%w: %s", err, strings.Join(s.ErrorMessages(), ", "))
}
return nil
}
// FindSecret gets a specific secret by its database ID.
func (db *Database) FindSecret(id interface{}) (*Secret, error) {
var secret Secret
if err := db.db.
Model(&Secret{}).
Where("id = ?", id).
First(&secret).
Error; err != nil {
return nil, fmt.Errorf("failed to find secret %v: %w", id, err)
}
return &secret, nil
}
// ListSecrets lists all secrets in the database.
func (db *Database) ListSecrets(scopes ...Scope) ([]*Secret, error) {
var secrets []*Secret
if err := db.db.
Scopes(scopes...).
Model(&Secret{}).
Find(&secrets).
Error; err != nil {
if IsNotFound(err) {
return secrets, nil
}
}
return secrets, nil
}
// ListSecretsForType lists all secrets for the given type, ordered by their
// creation date, but with inactive secrets (ones not ready to be used as
// primary) at the end of the list, allowing for propagation over time.
func (db *Database) ListSecretsForType(typ SecretType, scopes ...Scope) ([]*Secret, error) {
scopes = append(scopes, InConsumableSecretOrder())
var secrets []*Secret
if err := db.db.
Scopes(scopes...).
Model(&Secret{}).
Where("secrets.type = ?", typ).
Find(&secrets).
Error; err != nil {
if IsNotFound(err) {
return secrets, nil
}
}
return secrets, nil
}
// ActivateSecrets activates all secrets that are not currently activate but
// have been created for since the provided timestamp.
func (db *Database) ActivateSecrets(typ SecretType, since time.Time) error {
if err := db.db.
Model(&Secret{}).
Where("active IS FALSE AND type = ? AND created_at < ?", typ, since).
UpdateColumn("active", true).
Error; err != nil && !IsNotFound(err) {
return fmt.Errorf("failed to activate secrets: %w", err)
}
return nil
}
// SaveSecret creates or updates the secret.
func (db *Database) SaveSecret(s *Secret, actor Auditable) error {
if s == nil {
return fmt.Errorf("provided secret is nil")
}
if actor == nil {
return fmt.Errorf("auditing actor is nil")
}
return db.db.Transaction(func(tx *gorm.DB) error {
var audits []*AuditEntry
var existing Secret
if err := tx.
Unscoped().
Model(&Secret{}).
Where("id = ?", s.ID).
First(&existing).
Error; err != nil && !IsNotFound(err) {
return fmt.Errorf("failed to get existing secret: %w", err)
}
// Save the record
if err := tx.Unscoped().Save(s).Error; err != nil {
return err
}
// New record?
if existing.ID == 0 {
audit := BuildAuditEntry(actor, "created secret", s, 0)
audits = append(audits, audit)
} else {
if existing.Type != s.Type {
audit := BuildAuditEntry(actor, "updated secret type", s, 0)
audit.Diff = stringDiff(string(existing.Type), string(s.Type))
audits = append(audits, audit)
}
if existing.Reference != s.Reference {
audit := BuildAuditEntry(actor, "updated secret reference", s, 0)
audit.Diff = stringDiff(existing.Reference, s.Reference)
audits = append(audits, audit)
}
if existing.Active != s.Active {
audit := BuildAuditEntry(actor, "updated secret active status", s, 0)
audit.Diff = boolDiff(existing.Active, s.Active)
audits = append(audits, audit)
}
}
// Save all audits
for _, audit := range audits {
if err := tx.Save(audit).Error; err != nil {
return fmt.Errorf("failed to save audits: %w", err)
}
}
return nil
})
}
// DeleteSecret performs a soft delete on the provided secret.
func (db *Database) DeleteSecret(s *Secret, actor Auditable) error {
return db.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Delete(s).Error; err != nil && !IsNotFound(err) {
return fmt.Errorf("failed to delete secret: %w", err)
}
audit := BuildAuditEntry(actor, "marked secret for deletion", s, 0)
if err := tx.Save(audit).Error; err != nil {
return fmt.Errorf("failed to save audits: %w", err)
}
return nil
})
}
// PurgeSecret deletes the secret for real.
func (db *Database) PurgeSecret(s *Secret, actor Auditable) error {
return db.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Delete(s).Error; err != nil && !IsNotFound(err) {
return fmt.Errorf("failed to purge secret: %w", err)
}
audit := BuildAuditEntry(actor, "purged secret", s, 0)
if err := tx.Save(audit).Error; err != nil {
return fmt.Errorf("failed to save audits: %w", err)
}
return nil
})
}