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  }