go.chromium.org/luci@v0.0.0-20250314024836-d9a61d0730e6/tokenserver/appengine/impl/utils/policy/policy.go (about) 1 // Copyright 2017 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 policy 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/hex" 21 "time" 22 23 "google.golang.org/protobuf/proto" 24 25 "go.chromium.org/luci/common/data/caching/lazyslot" 26 "go.chromium.org/luci/common/data/rand/mathrand" 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/config/validation" 30 ) 31 32 // ErrNoPolicy is returned by Queryable(...) if a policy is not yet available. 33 // 34 // This happens when the service is deployed for the first time and policy 35 // configs aren't fetched yet. This error will not show up if ImportConfigs 36 // succeeded at least once. 37 var ErrNoPolicy = errors.New("policy config is not imported yet") 38 39 // Policy describes how to fetch, store and parse policy documents. 40 // 41 // This is a singleton-like object that should be shared by multiple requests. 42 // 43 // Each instance corresponds to one kind of a policy and it keeps a Queryable 44 // form if the corresponding policy cached in local memory, occasionally 45 // updating it based on the configs stored in the datastore (that are in turn 46 // periodically updated from a cron). 47 type Policy struct { 48 // Name defines the name of the policy, e.g. "delegation rules". 49 // 50 // It is used in datastore IDs and for logging. 51 Name string 52 53 // Fetch fetches and parses all relevant text proto files. 54 // 55 // This is a user-supplied callback. 56 // 57 // Called from cron when ingesting new configs. It must return either a non 58 // empty bundle with configs or an error. 59 Fetch func(c context.Context, f ConfigFetcher) (ConfigBundle, error) 60 61 // Validate verifies the fetched config files are semantically valid. 62 // 63 // This is a user-supplied callback. Must be a pure function. 64 // 65 // Reports all errors through the given validation.Context object. The config 66 // is considered valid if there are no errors reported. A valid config must be 67 // accepted by Prepare without errors. 68 // 69 // Called from cron when ingesting new configs. 70 Validate func(v *validation.Context, cfg ConfigBundle) 71 72 // Prepare converts validated configs into an optimized queryable form. 73 // 74 // This is a user-supplied callback. Must be a pure function. 75 // 76 // The result of the processing is cached in local instance memory for 1 min. 77 // It is supposed to be a read-only object, optimized for performing queries 78 // over it. 79 // 80 // Users of Policy should type-cast it to an appropriate type. 81 Prepare func(c context.Context, cfg ConfigBundle, revision string) (Queryable, error) 82 83 cache lazyslot.Slot // holds and updates in-memory cache of Queryable 84 } 85 86 // Queryable is validated and parsed configs in a form optimized for queries. 87 // 88 // This object is shared between multiple requests and kept in memory for as 89 // long as it still matches the current config. 90 type Queryable interface { 91 // ConfigRevision returns the revision passed to Policy.Prepare. 92 // 93 // It is a revision of configs used to construct this object. Used for 94 // logging. 95 ConfigRevision() string 96 } 97 98 // ConfigFetcher hides details of interaction with LUCI Config. 99 // 100 // Passed to Fetch callback. 101 type ConfigFetcher interface { 102 // FetchTextProto fetches text-serialized protobuf message at a given path. 103 // 104 // The path is relative to the token server config set root in LUCI config. 105 // 106 // On success returns nil and fills in 'out' (which should be a pointer to 107 // a concrete proto message class). May return transient error (e.g. timeouts) 108 // and fatal ones (e.g. bad proto file). 109 FetchTextProto(c context.Context, path string, out proto.Message) error 110 } 111 112 // ImportConfigs updates configs stored in the datastore. 113 // 114 // Is should be periodically called from a cron. 115 // 116 // Returns the revision of the configs that are now in the datastore. It's 117 // either the imported revision, if configs change, or a previously known 118 // revision, if configs at HEAD are same. 119 // 120 // Validation errors are returned as *validation.Error struct. Use type cast to 121 // sniff them, if necessary. 122 func (p *Policy) ImportConfigs(c context.Context) (rev string, err error) { 123 c = logging.SetField(c, "policy", p.Name) 124 125 // Fetch and parse text protos stored in LUCI config. The fetcher will also 126 // record the revision of the fetched files. 127 fetcher := luciConfigFetcher{} 128 bundle, err := p.Fetch(c, &fetcher) 129 if err == nil && len(bundle) == 0 { 130 err = errors.New("no configs fetched by the callback") 131 } 132 if err != nil { 133 return "", errors.Annotate(err, "failed to fetch policy configs").Err() 134 } 135 rev = fetcher.Revision() 136 137 // Convert configs into a form appropriate for the datastore. We'll skip the 138 // rest of the import if this exact blob is already in the datastore (based on 139 // SHA256 digest). 140 cfgBlob, err := serializeBundle(bundle) 141 if err != nil { 142 return "", errors.Annotate(err, "failed to serialize the configs").Err() 143 } 144 digest := sha256.Sum256(cfgBlob) 145 digestHex := hex.EncodeToString(digest[:]) 146 logging.Infof(c, "Got %d bytes of configs (SHA256 %s)", len(cfgBlob), digestHex) 147 148 // Do we have it already? 149 existingHdr, err := getImportedPolicyHeader(c, p.Name) 150 if err != nil { 151 return "", errors.Annotate(err, "failed to grab ImportedPolicyHeader").Err() 152 } 153 if existingHdr != nil && digestHex == existingHdr.SHA256 { 154 logging.Infof( 155 c, "Configs are up-to-date. Last changed at rev %s, last checked rev is %s.", 156 existingHdr.Revision, rev) 157 return existingHdr.Revision, nil 158 } 159 160 existingRev := "(nil)" 161 if existingHdr != nil { 162 existingRev = existingHdr.Revision 163 } 164 logging.Infof(c, "Policy config changed: %s -> %s", existingRev, rev) 165 166 if p.Validate != nil { 167 ctx := &validation.Context{Context: c} 168 p.Validate(ctx, bundle) 169 if err := ctx.Finalize(); err != nil { 170 return "", errors.Annotate(err, "configs at rev %s are invalid", rev).Err() 171 } 172 } 173 174 // Double check that they actually can be parsed into a queryable form. If 175 // not, the Policy callbacks are buggy. 176 queriable, err := p.Prepare(c, bundle, rev) 177 if err == nil && queriable.ConfigRevision() != rev { 178 err = errors.New("wrong revision in result of Prepare callback") 179 } 180 if err != nil { 181 return "", errors.Annotate(err, "failed to convert configs into a queryable form").Err() 182 } 183 184 logging.Infof(c, "Storing new configs") 185 if err := updateImportedPolicy(c, p.Name, rev, digestHex, cfgBlob); err != nil { 186 return "", err 187 } 188 189 return rev, nil 190 } 191 192 // Queryable returns a form of the policy document optimized for queries. 193 // 194 // This is hot function called from each RPC handler. It uses local in-memory 195 // cache to store the configs, synchronizing it with the state stored in the 196 // datastore once a minute. 197 // 198 // Returns ErrNoPolicy if the policy config wasn't imported yet. 199 func (p *Policy) Queryable(c context.Context) (Queryable, error) { 200 val, err := p.cache.Get(c, func(prev any) (newQ any, exp time.Duration, err error) { 201 prevQ, _ := prev.(Queryable) 202 newQ, err = p.grabQueryable(c, prevQ) 203 if err == nil { 204 exp = cacheExpiry(c) 205 } 206 return 207 }) 208 if err != nil { 209 return nil, err 210 } 211 return val.(Queryable), nil 212 } 213 214 // grabQueryable is called whenever cached Queryable in p.cache expires. 215 func (p *Policy) grabQueryable(c context.Context, prevQ Queryable) (Queryable, error) { 216 c = logging.SetField(c, "policy", p.Name) 217 218 logging.Infof(c, "Checking version of the policy in the datastore") 219 hdr, err := getImportedPolicyHeader(c, p.Name) 220 switch { 221 case err != nil: 222 return nil, errors.Annotate(err, "failed to fetch importedPolicyHeader entity").Err() 223 case hdr == nil: 224 return nil, ErrNoPolicy 225 } 226 227 // Reuse existing Queryable object if configs didn't change. 228 if prevQ != nil && prevQ.ConfigRevision() == hdr.Revision { 229 return prevQ, nil 230 } 231 232 // Fetch new configs. 233 logging.Infof(c, "Fetching policy configs from the datastore") 234 body, err := getImportedPolicyBody(c, p.Name) 235 switch { 236 case err != nil: 237 return nil, errors.Annotate(err, "failed to fetch importedPolicyBody entity").Err() 238 case body == nil: // this is rare, the body shouldn't disappear 239 logging.Errorf(c, "The policy body is unexpectedly gone") 240 return nil, ErrNoPolicy 241 } 242 243 // An error here and below can happen if previously validated config is no 244 // longer valid (e.g. if the service code is updated and new code doesn't like 245 // the stored config anymore). 246 // 247 // If this check fails, the service is effectively offline until configs are 248 // updated. Presumably, it is better than silently using no longer valid 249 // config. 250 logging.Infof(c, "Using configs at rev %s", body.Revision) 251 configs, unknown, err := deserializeBundle(body.Data) 252 if err != nil { 253 return nil, errors.Annotate(err, "failed to deserialize cached configs").Err() 254 } 255 if len(unknown) != 0 { 256 for _, cfg := range unknown { 257 logging.Errorf(c, "Unknown proto type %q in cached config %q", cfg.Kind, cfg.Path) 258 } 259 return nil, errors.New("failed to deserialize some cached configs") 260 } 261 queryable, err := p.Prepare(c, configs, body.Revision) 262 if err != nil { 263 return nil, errors.Annotate(err, "failed to process cached configs").Err() 264 } 265 266 return queryable, nil 267 } 268 269 // cacheExpiry returns a random duration from [4 min, 5 min). 270 // 271 // It's used to define when to refresh in-memory Queryable cache. We randomize 272 // it to desynchronize cache updates of different Policy instances. 273 func cacheExpiry(c context.Context) time.Duration { 274 rnd := time.Duration(mathrand.Int63n(c, int64(time.Minute))) 275 return 4*time.Minute + rnd 276 }