Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-db/go/oidc_client_config.go
2497 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package db
6
7
import (
8
"context"
9
"errors"
10
"fmt"
11
"sort"
12
"time"
13
14
"github.com/google/uuid"
15
"gorm.io/gorm"
16
)
17
18
type OIDCClientConfig struct {
19
ID uuid.UUID `gorm:"primary_key;column:id;type:char;size:36;" json:"id"`
20
21
OrganizationID uuid.UUID `gorm:"column:organizationId;type:char;size:36;" json:"organizationId"`
22
23
Issuer string `gorm:"column:issuer;type:char;size:255;" json:"issuer"`
24
25
Data EncryptedJSON[OIDCSpec] `gorm:"column:data;type:text;size:65535" json:"data"`
26
27
Active bool `gorm:"column:active;type:tinyint;default:0;" json:"active"`
28
29
Verified *bool `gorm:"column:verified;type:tinyint;default:0;" json:"verified"`
30
31
LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"`
32
// deleted is reserved for use by periodic deleter.
33
_ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"`
34
}
35
36
func BoolPointer(b bool) *bool {
37
return &b
38
}
39
40
func (c *OIDCClientConfig) TableName() string {
41
return "d_b_oidc_client_config"
42
}
43
44
// It feels wrong to have to define re-define all of these fields.
45
// However, I could not find a Go library which would include json annotations on the structs to guarantee the fields
46
// will remain consistent over time (and resilient to rename). If we find one, we can change this.
47
type OIDCSpec struct {
48
// ClientID is the application's ID.
49
ClientID string `json:"clientId"`
50
51
// ClientSecret is the application's secret.
52
ClientSecret string `json:"clientSecret"`
53
54
// RedirectURL is the URL to redirect users going through
55
// the OAuth flow, after the resource owner's URLs.
56
RedirectURL string `json:"redirectUrl"`
57
58
// Scope specifies optional requested permissions.
59
Scopes []string `json:"scopes"`
60
61
// CelExpression is an optional expression that can be used to determine if the client should be allowed to authenticate.
62
CelExpression string `json:"celExpression"`
63
64
// UsePKCE specifies if the client should use PKCE for the OAuth flow.
65
UsePKCE bool `json:"usePKCE"`
66
}
67
68
func CreateOIDCClientConfig(ctx context.Context, conn *gorm.DB, cfg OIDCClientConfig) (OIDCClientConfig, error) {
69
if cfg.ID == uuid.Nil {
70
return OIDCClientConfig{}, errors.New("ID must be set")
71
}
72
73
if cfg.OrganizationID == uuid.Nil {
74
return OIDCClientConfig{}, errors.New("organization ID must be set")
75
}
76
77
if cfg.Issuer == "" {
78
return OIDCClientConfig{}, errors.New("issuer must be set")
79
}
80
81
tx := conn.
82
WithContext(ctx).
83
Create(&cfg)
84
if tx.Error != nil {
85
return OIDCClientConfig{}, fmt.Errorf("failed to create oidc client config: %w", tx.Error)
86
}
87
88
return cfg, nil
89
}
90
91
func GetOIDCClientConfig(ctx context.Context, conn *gorm.DB, id uuid.UUID) (OIDCClientConfig, error) {
92
var config OIDCClientConfig
93
94
if id == uuid.Nil {
95
return OIDCClientConfig{}, fmt.Errorf("OIDC Client Config ID is a required argument")
96
}
97
98
tx := conn.
99
WithContext(ctx).
100
Where("id = ?", id).
101
Where("deleted = ?", 0).
102
First(&config)
103
if tx.Error != nil {
104
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
105
return OIDCClientConfig{}, fmt.Errorf("OIDC Client Config with ID %s does not exist: %w", id.String(), ErrorNotFound)
106
}
107
return OIDCClientConfig{}, fmt.Errorf("Failed to retrieve OIDC client config: %v", tx.Error)
108
}
109
110
return config, nil
111
}
112
113
func GetOIDCClientConfigForOrganization(ctx context.Context, conn *gorm.DB, id, organizationID uuid.UUID) (OIDCClientConfig, error) {
114
var config OIDCClientConfig
115
116
if id == uuid.Nil {
117
return OIDCClientConfig{}, fmt.Errorf("OIDC Client Config ID is a required argument")
118
}
119
120
if organizationID == uuid.Nil {
121
return OIDCClientConfig{}, fmt.Errorf("organization id is a required argument")
122
}
123
124
tx := conn.
125
WithContext(ctx).
126
Where("id = ?", id).
127
Where("organizationId = ?", organizationID).
128
Where("deleted = ?", 0).
129
First(&config)
130
if tx.Error != nil {
131
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
132
return OIDCClientConfig{}, fmt.Errorf("OIDC Client Config with ID %s for Organization ID %s does not exist: %w", id.String(), organizationID.String(), ErrorNotFound)
133
}
134
135
return OIDCClientConfig{}, fmt.Errorf("Failed to retrieve OIDC client config %s for Organization ID %s: %v", id.String(), organizationID.String(), tx.Error)
136
}
137
138
return config, nil
139
}
140
141
func ListOIDCClientConfigsForOrganization(ctx context.Context, conn *gorm.DB, organizationID uuid.UUID) ([]OIDCClientConfig, error) {
142
if organizationID == uuid.Nil {
143
return nil, errors.New("organization ID is a required argument")
144
}
145
146
var results []OIDCClientConfig
147
148
tx := conn.
149
WithContext(ctx).
150
Where("organizationId = ?", organizationID.String()).
151
Where("deleted = ?", 0).
152
Order("id").
153
Find(&results)
154
if tx.Error != nil {
155
return nil, fmt.Errorf("failed to list oidc client configs for organization %s: %w", organizationID.String(), tx.Error)
156
}
157
158
return results, nil
159
}
160
161
func DeleteOIDCClientConfig(ctx context.Context, conn *gorm.DB, id, organizationID uuid.UUID) error {
162
if id == uuid.Nil {
163
return fmt.Errorf("id is a required argument")
164
}
165
166
if organizationID == uuid.Nil {
167
return fmt.Errorf("organization id is a required argument")
168
}
169
170
tx := conn.
171
WithContext(ctx).
172
Table((&OIDCClientConfig{}).TableName()).
173
Where("id = ?", id).
174
Where("organizationId = ?", organizationID).
175
Where("deleted = ?", 0).
176
Update("deleted", 1)
177
178
if tx.Error != nil {
179
return fmt.Errorf("failed to delete oidc client config (ID: %s): %v", id.String(), tx.Error)
180
}
181
182
if tx.RowsAffected == 0 {
183
return fmt.Errorf("oidc client config ID: %s for organization ID: %s does not exist: %w", id.String(), organizationID.String(), ErrorNotFound)
184
}
185
186
return nil
187
}
188
189
func GetActiveOIDCClientConfigByOrgSlug(ctx context.Context, conn *gorm.DB, slug string) (OIDCClientConfig, error) {
190
var config OIDCClientConfig
191
192
if slug == "" {
193
return OIDCClientConfig{}, fmt.Errorf("slug is a required argument")
194
}
195
196
tx := conn.
197
WithContext(ctx).
198
Table(fmt.Sprintf("%s as config", (&OIDCClientConfig{}).TableName())).
199
Joins(fmt.Sprintf("JOIN %s AS team ON team.id = config.organizationId", (&Organization{}).TableName())).
200
Where("team.slug = ?", slug).
201
Where("config.deleted = ?", 0).
202
Where("config.active = ?", 1).
203
First(&config)
204
205
if tx.Error != nil {
206
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
207
return OIDCClientConfig{}, fmt.Errorf("OIDC Client Config for Organization (slug: %s) does not exist: %w", slug, ErrorNotFound)
208
}
209
return OIDCClientConfig{}, fmt.Errorf("Failed to retrieve OIDC client config: %v", tx.Error)
210
}
211
212
return config, nil
213
}
214
215
// UpdateOIDCClientConfig performs an update of the OIDC Client config.
216
// Only non-zero fields specified in the struct are updated.
217
// When updating the encrypted contents of the specUpdate, you can specify them in the update to have re-encrypted in a transaction.
218
func UpdateOIDCClientConfig(ctx context.Context, conn *gorm.DB, cipher Cipher, update OIDCClientConfig, specUpdate *OIDCSpec) error {
219
if update.ID == uuid.Nil {
220
return errors.New("id is a required field")
221
}
222
223
txErr := conn.
224
WithContext(ctx).
225
Transaction(func(tx *gorm.DB) error {
226
if specUpdate != nil {
227
// we also need to update the contents of the encrypted spec.
228
existing, err := GetOIDCClientConfig(ctx, conn, update.ID)
229
if err != nil {
230
return err
231
}
232
233
decrypted, err := existing.Data.Decrypt(cipher)
234
if err != nil {
235
return fmt.Errorf("failed to decrypt oidc spec: %w", err)
236
}
237
238
updatedSpec := partialUpdateOIDCSpec(decrypted, *specUpdate)
239
240
encrypted, err := EncryptJSON(cipher, updatedSpec)
241
if err != nil {
242
return fmt.Errorf("failed to encrypt oidc spec: %w", err)
243
}
244
245
// Set the serialized contents on our desired update object
246
update.Data = encrypted
247
248
// Each update should unverify the entry
249
update.Verified = BoolPointer(false)
250
}
251
252
updateTx := tx.
253
Model(&OIDCClientConfig{}).
254
Where("id = ?", update.ID.String()).
255
Where("deleted = ?", 0).
256
Updates(update)
257
if updateTx.Error != nil {
258
return fmt.Errorf("failed to update OIDC client: %w", updateTx.Error)
259
}
260
261
if updateTx.RowsAffected == 0 {
262
// FIXME(at) this should not return an error in case of empty update
263
return fmt.Errorf("OIDC client config ID: %s does not exist: %w", update.ID.String(), ErrorNotFound)
264
}
265
266
// return nil will commit the whole transaction
267
return nil
268
})
269
270
if txErr != nil {
271
return fmt.Errorf("failed to update oidc spec ID: %s: %w", update.ID.String(), txErr)
272
}
273
274
return nil
275
}
276
277
func SetClientConfigActiviation(ctx context.Context, conn *gorm.DB, id uuid.UUID, active bool) error {
278
config, err := GetOIDCClientConfig(ctx, conn, id)
279
if err != nil {
280
return err
281
}
282
283
value := 0
284
if active {
285
value = 1
286
}
287
288
tx := conn.
289
WithContext(ctx).
290
Table((&OIDCClientConfig{}).TableName()).
291
Where("id = ?", id.String()).
292
Update("active", value)
293
if tx.Error != nil {
294
return fmt.Errorf("failed to set oidc client config as active to %d (id: %s): %v", value, id.String(), tx.Error)
295
}
296
297
if active {
298
tx := conn.
299
WithContext(ctx).
300
Table((&OIDCClientConfig{}).TableName()).
301
Where("id != ?", id.String()).
302
Where("organizationId = ?", config.OrganizationID).
303
Where("deleted = ?", 0).
304
Update("active", 0)
305
if tx.Error != nil {
306
return fmt.Errorf("failed to set other oidc client configs as inactive: %v", tx.Error)
307
}
308
}
309
310
return nil
311
}
312
313
func VerifyClientConfig(ctx context.Context, conn *gorm.DB, id uuid.UUID) error {
314
return setClientConfigVerifiedFlag(ctx, conn, id, true)
315
}
316
317
func UnverifyClientConfig(ctx context.Context, conn *gorm.DB, id uuid.UUID) error {
318
return setClientConfigVerifiedFlag(ctx, conn, id, false)
319
}
320
321
func setClientConfigVerifiedFlag(ctx context.Context, conn *gorm.DB, id uuid.UUID, verified bool) error {
322
_, err := GetOIDCClientConfig(ctx, conn, id)
323
if err != nil {
324
return err
325
}
326
327
value := 0
328
if verified {
329
value = 1
330
}
331
332
tx := conn.
333
WithContext(ctx).
334
Table((&OIDCClientConfig{}).TableName()).
335
Where("id = ?", id.String()).
336
Update("verified", value)
337
if tx.Error != nil {
338
return fmt.Errorf("failed to set oidc client config as active to %d (id: %s): %v", value, id.String(), tx.Error)
339
}
340
341
return nil
342
}
343
344
func partialUpdateOIDCSpec(old, new OIDCSpec) OIDCSpec {
345
if new.ClientID != "" {
346
old.ClientID = new.ClientID
347
}
348
349
if new.ClientSecret != "" {
350
old.ClientSecret = new.ClientSecret
351
}
352
353
if new.RedirectURL != "" {
354
old.RedirectURL = new.RedirectURL
355
}
356
357
old.CelExpression = new.CelExpression
358
old.UsePKCE = new.UsePKCE
359
360
if !oidcScopesEqual(old.Scopes, new.Scopes) {
361
old.Scopes = new.Scopes
362
}
363
364
return old
365
}
366
367
func oidcScopesEqual(old, new []string) bool {
368
if len(old) != len(new) {
369
return false
370
}
371
372
sort.Strings(old)
373
sort.Strings(new)
374
375
for i := 0; i < len(old); i++ {
376
if old[i] != new[i] {
377
return false
378
}
379
}
380
381
return true
382
}
383
384