blob: e81516d8a7e9a0d1191b719912dfffbbe9df2f9e [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package criadb provides a wrapper around the CrIA authorization database. The
// database is replicated from a GCP Cloud Storage bucket, and is updated every
// 30 + rand.Intn(10) seconds. This database is used to check whether a user is
// a member of a specified mdb (or other) group.
package criadb
import (
"context"
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"sync"
"time"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/auth/authtest"
"go.chromium.org/luci/server/auth/authdb/dump"
"go.chromium.org/luci/server/caching"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
type AuthDatabase struct {
mu sync.RWMutex
db authdb.DB
serviceName string
cache *caching.ProcessCacheData
authConfig *auth.Config
}
func (db *AuthDatabase) updateDB() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
ctx = caching.WithProcessCacheData(ctx, db.cache)
ctx = auth.Initialize(ctx, db.authConfig)
fetcher := dump.Fetcher{
StorageDumpPath: fmt.Sprintf("%s.appspot.com/auth-db", db.serviceName),
AuthServiceURL: fmt.Sprintf("https://%s.appspot.com", db.serviceName),
AuthServiceAccount: fmt.Sprintf("%s@appspot.gserviceaccount.com", db.serviceName),
OAuthScopes: auth.CloudOAuthScopes,
}
db.mu.RLock()
curSnap, _ := db.db.(*authdb.SnapshotDB)
snap, err := fetcher.FetchAuthDB(ctx, curSnap)
if err != nil {
db.mu.RUnlock()
return err
}
db.mu.RUnlock()
db.mu.Lock()
db.db = snap
db.mu.Unlock()
return nil
}
func NewDatabase(serviceName string) (*AuthDatabase, error) {
creds, err := google.FindDefaultCredentials(context.Background(), auth.CloudOAuthScopes...)
if err != nil {
return nil, err
}
db := &AuthDatabase{
serviceName: serviceName,
cache: caching.NewProcessCacheData(),
authConfig: &auth.Config{
AccessTokenProvider: func(ctx context.Context, scopes []string) (*oauth2.Token, error) {
return creds.TokenSource.Token()
},
IDTokenProvider: func(ctx context.Context, audience string) (*oauth2.Token, error) {
return creds.TokenSource.Token()
},
AnonymousTransport: func(context.Context) http.RoundTripper {
return http.DefaultTransport
},
},
}
if err := db.updateDB(); err != nil {
return nil, err
}
go func() {
// TODO(roland): this will not fail gracefully, especially during shutdown.
for {
jitter := time.Duration(rand.Int63n(int64(10 * time.Second)))
time.Sleep(30*time.Second + jitter)
if err := db.updateDB(); err != nil {
log.Printf("db.updateDB failed: %s", err)
}
}
}()
return db, nil
}
func (db *AuthDatabase) IsMemberOfAny(ctx context.Context, ident string, groups []string) (bool, error) {
db.mu.RLock()
defer db.mu.RUnlock()
if db.db == nil {
return false, errors.New("authdb uninitialized")
}
return db.db.IsMember(ctx, identity.Identity(ident), groups)
}
func NewDevDatabase() *AuthDatabase {
return &AuthDatabase{db: authdb.DevServerDB{}}
}
func NewTestDatabase(memberships [][2]string) *AuthDatabase {
var datums []authtest.MockedDatum
for _, membership := range memberships {
datums = append(datums, authtest.MockMembership(identity.Identity(membership[0]), membership[1]))
}
return &AuthDatabase{db: authtest.NewFakeDB(datums...)}
}