go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/authdb.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package impl 16 17 import ( 18 "context" 19 "sync" 20 "time" 21 22 "go.opentelemetry.io/otel" 23 "go.opentelemetry.io/otel/codes" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/server/auth/authdb" 29 30 "go.chromium.org/luci/auth_service/impl/model" 31 ) 32 33 var tracer = otel.Tracer("go.chromium.org/luci/auth_service") 34 35 // AuthDBProvider knows how to produce an up-to-date authdb.DB instance. 36 // 37 // It caches it in memory, refetching it from Datastore when it detects the 38 // cached copy is stale. 39 type AuthDBProvider struct { 40 m sync.RWMutex 41 cached *authdb.SnapshotDB 42 } 43 44 // GetAuthDB returns the latest authdb.DB instance to use for ACL checks. 45 // 46 // Refetches it from the datastore if necessary. 47 func (a *AuthDBProvider) GetAuthDB(ctx context.Context) (db authdb.DB, err error) { 48 ctx, span := tracer.Start(ctx, "go.chromium.org/luci/auth_service/impl/GetAuthDB") 49 defer func() { 50 if err != nil { 51 span.RecordError(err) 52 span.SetStatus(codes.Error, err.Error()) 53 } 54 span.End() 55 }() 56 57 // Grab the latest AuthDB revision number in the datastore. 58 latestState, err := model.GetReplicationState(ctx) 59 if err != nil { 60 return nil, errors.Annotate(err, "failed to check the latest AuthDB revision").Err() 61 } 62 63 // Use the cached copy if it is up-to-date. 64 a.m.RLock() 65 cached := a.cached 66 a.m.RUnlock() 67 if cached != nil && cached.Rev == latestState.AuthDBRev { 68 return cached, nil 69 } 70 71 if cached == nil { 72 logging.Infof(ctx, "Initializing AuthDB") 73 } else { 74 logging.Infof(ctx, "Refreshing AuthDB (have rev %d, want rev >=%d)", cached.Rev, latestState.AuthDBRev) 75 } 76 77 // Fetch the fresh copy from the datastore. Make all other callers of 78 // GetAuthDB wait to avoid all of them hitting the datastore at once when 79 // the cached copy expires. 80 a.m.Lock() 81 defer a.m.Unlock() 82 83 // Maybe someone else already fetched a fresher copy while we were waiting 84 // on the lock. 85 if a.cached != nil && a.cached.Rev >= latestState.AuthDBRev { 86 logging.Infof(ctx, "Other goroutine fetched AuthDB rev %d already", a.cached.Rev) 87 return a.cached, nil 88 } 89 90 logging.Infof(ctx, "Fetching AuthDB from the datastore") 91 92 // Transactionally fetch all data (including AuthReplicationState with the 93 // freshest revision) and convert it into an authdb.SnapshotDB. 94 snap, err := model.TakeSnapshot(ctx) 95 if err != nil { 96 return nil, errors.Annotate(err, "failed to make AuthDB snapshot").Err() 97 } 98 // The permissions maintained by the Python version should be used 99 // instead if the update-realms cron is in dry run mode. 100 useV1Perms := model.ParseDryRunEnvVar(model.DryRunCronRealmsEnvVar) 101 snapDB, err := snap.ToAuthDB(useV1Perms) 102 if err != nil { 103 return nil, errors.Annotate(err, "failed to process AuthDB snapshot").Err() 104 } 105 106 logging.Infof(ctx, "Fetched AuthDB rev %d", snapDB.Rev) 107 108 a.cached = snapDB 109 return snapDB, nil 110 } 111 112 // RefreshPeriodically runs a loop that periodically refreshes the cached copy 113 // of AuthDB. 114 func (a *AuthDBProvider) RefreshPeriodically(ctx context.Context) { 115 for { 116 if r := <-clock.After(ctx, 30*time.Second); r.Err != nil { 117 return // the context is canceled 118 } 119 if _, err := a.GetAuthDB(ctx); err != nil { 120 logging.Warningf(ctx, "Failed to refresh AuthDB: %s", err) 121 } 122 } 123 }