go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/logPrefix.go (about)

     1  // Copyright 2015 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 coordinator
    16  
    17  import (
    18  	"context"
    19  	"crypto/subtle"
    20  	"errors"
    21  	"fmt"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/common/logging"
    26  	ds "go.chromium.org/luci/gae/service/datastore"
    27  	"go.chromium.org/luci/logdog/common/types"
    28  )
    29  
    30  const (
    31  	// RegistrationNonceTimeout is how long LogPrefix.IsRetry will consider
    32  	// a matching nonce to be valid.
    33  	RegistrationNonceTimeout = 15 * time.Minute
    34  )
    35  
    36  // LogPrefix is a datastore model for a prefix space. All log streams sharing
    37  // a prefix will have a LogPrefix entry to group under.
    38  //
    39  // A LogPrefix is keyed on the hash of its Prefix property.
    40  //
    41  // Prefix-scoped properties are used to control creation and modification
    42  // attributes of log streams sharing the prefix.
    43  type LogPrefix struct {
    44  	// ID is the LogPrefix's ID. It is an encoded hash value generated from the
    45  	// stream's Prefix field.
    46  	ID HashID `gae:"$id"`
    47  
    48  	// Schema is the datastore schema version for this object. This can be used
    49  	// to facilitate schema migrations.
    50  	//
    51  	// The current schema is currentSchemaVersion.
    52  	Schema string
    53  
    54  	// Created is the time when this stream was created.
    55  	Created time.Time `gae:",noindex"`
    56  
    57  	// Prefix is this log stream's prefix value. Log streams with the same prefix
    58  	// are logically grouped.
    59  	//
    60  	// This value should not be changed once populated, as it will invalidate the
    61  	// HashID.
    62  	Prefix string `gae:",noindex"`
    63  
    64  	// Realm is a full realm name ("<project>:<realm>") with ACLs for this prefix.
    65  	//
    66  	// It is set in RegisterStream and can't be changed afterwards.
    67  	Realm string
    68  
    69  	// Source is the (indexed) set of source strings sent by the prefix registrar.
    70  	Source []string
    71  
    72  	// Expiration is the time when this log prefix expires. Stream registrations
    73  	// for this prefix will fail after this point.
    74  	Expiration time.Time
    75  
    76  	// Secret is the Butler secret value for this prefix. All streams within
    77  	// the prefix share this secret value.
    78  	//
    79  	// This value may only be returned to LogDog services; it is not user-visible.
    80  	Secret []byte `gae:",noindex"`
    81  
    82  	// OpNonce is provided by the client when calling RegisterPrefix. If the
    83  	// client provides the same nonce on a subsequent invocation of
    84  	// RegisterPrefix, the server will respond with success instead of
    85  	// AlreadyExists.
    86  	//
    87  	// This must have a length of either 0 or types.OpNonceLength.
    88  	//
    89  	// The nonce has a valid lifetime of RegistrationNonceTimeout after Created.
    90  	OpNonce []byte `gae:",noindex"`
    91  
    92  	// extra causes datastore to ignore unrecognized fields and strip them in
    93  	// future writes.
    94  	extra ds.PropertyMap `gae:"-,extra"`
    95  }
    96  
    97  var _ interface {
    98  	ds.PropertyLoadSaver
    99  } = (*LogPrefix)(nil)
   100  
   101  // LogPrefixID returns the HashID for a specific prefix.
   102  func LogPrefixID(prefix types.StreamName) HashID {
   103  	return makeHashID(string(prefix))
   104  }
   105  
   106  // Load implements ds.PropertyLoadSaver.
   107  func (p *LogPrefix) Load(pmap ds.PropertyMap) error {
   108  	if err := ds.GetPLS(p).Load(pmap); err != nil {
   109  		return err
   110  	}
   111  
   112  	// Validate the log prefix. Don't enforce HashID correctness, since datastore
   113  	// hasn't populated that field yet.
   114  	return p.validateImpl(false)
   115  }
   116  
   117  // Save implements ds.PropertyLoadSaver.
   118  func (p *LogPrefix) Save(withMeta bool) (ds.PropertyMap, error) {
   119  	if err := p.validateImpl(true); err != nil {
   120  		return nil, err
   121  	}
   122  	p.Schema = CurrentSchemaVersion
   123  
   124  	return ds.GetPLS(p).Save(withMeta)
   125  }
   126  
   127  // IsRetry checks to see if this LogPrefix is still in the OpNonce
   128  // window, and if nonce matches the one in this LogPrefix.
   129  func (p *LogPrefix) IsRetry(c context.Context, nonce []byte) bool {
   130  	if len(nonce) != types.OpNonceLength {
   131  		logging.Infof(c, "user provided invalid nonce length (%d)", len(nonce))
   132  		return false
   133  	}
   134  	if len(p.OpNonce) == 0 {
   135  		logging.Infof(c, "prefix %q has no associated nonce", p.Prefix)
   136  		return false
   137  	}
   138  	if clock.Now(c).After(p.Created.Add(RegistrationNonceTimeout)) {
   139  		logging.Infof(c, "prefix %q has expired nonce", p.Prefix)
   140  		return false
   141  	}
   142  	return subtle.ConstantTimeCompare(p.OpNonce, nonce) == 1
   143  }
   144  
   145  // getIDFromPrefix calculates the log stream's hash ID from its Prefix/Name
   146  // fields, which must be populated else this function will panic.
   147  func (p *LogPrefix) getIDFromPrefix() HashID {
   148  	if p.Prefix == "" {
   149  		panic("empty prefix")
   150  	}
   151  	return makeHashID(p.Prefix)
   152  }
   153  
   154  // Validate evaluates the state and data contents of the LogPrefix and returns
   155  // an error if it is invalid.
   156  func (p *LogPrefix) Validate() error {
   157  	return p.validateImpl(true)
   158  }
   159  
   160  func (p *LogPrefix) validateImpl(enforceHashID bool) error {
   161  	if enforceHashID {
   162  		// Make sure our Prefix and Name match the Hash ID.
   163  		if hid := p.getIDFromPrefix(); hid != p.ID {
   164  			return fmt.Errorf("hash IDs don't match (%q != %q)", hid, p.ID)
   165  		}
   166  	}
   167  
   168  	if err := types.StreamName(p.Prefix).Validate(); err != nil {
   169  		return fmt.Errorf("invalid prefix: %s", err)
   170  	}
   171  	if err := types.PrefixSecret(p.Secret).Validate(); err != nil {
   172  		return fmt.Errorf("invalid prefix secret: %s", err)
   173  	}
   174  	if p.Created.IsZero() {
   175  		return errors.New("created time is not set")
   176  	}
   177  	if l := len(p.OpNonce); l > 0 && l != types.OpNonceLength {
   178  		return fmt.Errorf("registration nonce has bad length (%d)", l)
   179  	}
   180  	return nil
   181  }