go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/quota/internal/quotakeys/keys.go (about)

     1  // Copyright 2022 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 quotakeys has utility functions for generating internal quota Redis
    16  // keys.
    17  package quotakeys
    18  
    19  import (
    20  	"strconv"
    21  	"strings"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/reflect/protoreflect"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  
    28  	"go.chromium.org/luci/server/quota/quotapb"
    29  )
    30  
    31  const (
    32  	// QuotaFieldDelim is used to delimit sections of keys which are user provided values.
    33  	//
    34  	// NOTE: this is ascii85-safe.
    35  	QuotaFieldDelim = "~"
    36  
    37  	// redisKeyPrefix is the prefix used used by ALL Redis keys in this module.
    38  	//
    39  	// It'll be repeated many times in the db, so we picked `"a` (i.e quote-ah)...
    40  	// Unique? Short? Cute? Obscure? Yes.
    41  	redisKeyPrefix = `"a`
    42  
    43  	accountKeyPrefix      = redisKeyPrefix + QuotaFieldDelim + "a" + QuotaFieldDelim
    44  	policyConfigPrefix    = redisKeyPrefix + QuotaFieldDelim + "p" + QuotaFieldDelim
    45  	requestDedupKeyPrefix = redisKeyPrefix + QuotaFieldDelim + "r" + QuotaFieldDelim
    46  )
    47  
    48  // parseRedisKey will parse a quota library redis key, and populate the given
    49  // Key proto message with its contents.
    50  //
    51  // This function assumes that the key proto has N string fields which have the
    52  // proto field numbers 1..N.
    53  func parseRedisKey(key, prefix string, to proto.Message) error {
    54  	if !strings.HasPrefix(key, prefix) {
    55  		return errors.New("incorrect prefix")
    56  	}
    57  	toks := strings.Split(key[len(prefix):], QuotaFieldDelim)
    58  	p := to.ProtoReflect()
    59  	fields := p.Descriptor().Fields()
    60  	if fields.Len() != len(toks) {
    61  		return errors.Reason("incorrect number of sections: %d != %d", len(toks), fields.Len()).Err()
    62  	}
    63  
    64  	for i, tok := range toks {
    65  		fn := protoreflect.FieldNumber(i + 1)
    66  		fd := fields.ByNumber(fn)
    67  		// HACK: Support uint for PolicyConfigID
    68  		if fd.Kind() == protoreflect.Uint32Kind {
    69  			val, err := strconv.ParseUint(tok, 10, 32)
    70  			if err != nil {
    71  				return errors.Annotate(err, "bad version scheme").Err()
    72  			}
    73  			p.Set(fd, protoreflect.ValueOfUint32(uint32(val)))
    74  		} else {
    75  			// assume string
    76  			p.Set(fd, protoreflect.ValueOfString(tok))
    77  		}
    78  	}
    79  
    80  	return to.(interface{ ValidateAll() error }).ValidateAll()
    81  }
    82  
    83  // serializeRedisKey generates
    84  func serializeRedisKey(prefix string, from proto.Message) string {
    85  	p := from.ProtoReflect()
    86  	fields := p.Descriptor().Fields()
    87  	fLen := fields.Len()
    88  	bld := strings.Builder{}
    89  	bld.WriteString(prefix)
    90  	for fNum := protoreflect.FieldNumber(1); int(fNum) <= fLen; fNum++ {
    91  		if fNum > 1 {
    92  			bld.WriteString(QuotaFieldDelim)
    93  		}
    94  		bld.WriteString(p.Get(fields.ByNumber(fNum)).String())
    95  	}
    96  	return bld.String()
    97  }
    98  
    99  // ParseAccountKey parses a raw key string and extracts a AccountID
   100  // from it (or returns an error).
   101  func ParseAccountKey(key string) (*quotapb.AccountID, error) {
   102  	ret := &quotapb.AccountID{}
   103  	err := parseRedisKey(key, accountKeyPrefix, ret)
   104  	return ret, err
   105  }
   106  
   107  // AccountKey returns the full redis key for an account.
   108  func AccountKey(id *quotapb.AccountID) string {
   109  	return serializeRedisKey(accountKeyPrefix, id)
   110  }
   111  
   112  // ParsePolicyConfigID parses a raw key string and extracts a PolicyConfigID
   113  // from it (or returns an error).
   114  func ParsePolicyConfigID(policyConfigID string) (*quotapb.PolicyConfigID, error) {
   115  	ret := &quotapb.PolicyConfigID{}
   116  	err := parseRedisKey(policyConfigID, policyConfigPrefix, ret)
   117  	return ret, err
   118  }
   119  
   120  // PolicyConfigID returns a full redis key for the given PolicyConfigID.
   121  //
   122  // `id` must already be validated, or this could panic.
   123  func PolicyConfigID(id *quotapb.PolicyConfigID) string {
   124  	return serializeRedisKey(policyConfigPrefix, id)
   125  }
   126  
   127  // ParsePolicyKey parses a raw key string and extracts a PolicyKey
   128  // from it (or returns an error).
   129  func ParsePolicyKey(policyKey string) (*quotapb.PolicyKey, error) {
   130  	ret := &quotapb.PolicyKey{}
   131  	err := parseRedisKey(policyKey, "", ret)
   132  	return ret, err
   133  }
   134  
   135  // PolicyKey returns a full redis key for the given PolicyKey.
   136  //
   137  // `id` must already be validated, or this could panic.
   138  func PolicyKey(id *quotapb.PolicyKey) string {
   139  	return serializeRedisKey("", id)
   140  }
   141  
   142  // ParsePolicyRef parses a raw PolicyRef and extracts a PolicyID
   143  // from it (or returns an error).
   144  func ParsePolicyRef(ref *quotapb.PolicyRef) (ret *quotapb.PolicyID, err error) {
   145  	ret = &quotapb.PolicyID{}
   146  	ret.Config, err = ParsePolicyConfigID(ref.Config)
   147  	if err == nil {
   148  		ret.Key, err = ParsePolicyKey(ref.Key)
   149  	}
   150  	return ret, err
   151  }
   152  
   153  // PolicyRef returns a PolicyRef for the given PolicyID.
   154  //
   155  // `id` must already be validated, or this could panic.
   156  func PolicyRef(id *quotapb.PolicyID) *quotapb.PolicyRef {
   157  	return &quotapb.PolicyRef{
   158  		Config: PolicyConfigID(id.Config),
   159  		Key:    PolicyKey(id.Key),
   160  	}
   161  }
   162  
   163  // ParseRequestDedupKey parses a raw key string and extracts the userID and requestID
   164  // from the request key.
   165  func ParseRequestDedupKey(key string) (*quotapb.RequestDedupKey, error) {
   166  	ret := &quotapb.RequestDedupKey{}
   167  	err := parseRedisKey(key, requestDedupKeyPrefix, ret)
   168  	return ret, err
   169  }
   170  
   171  // RequestDedupKey returns the full redis key for a request dedup entry.
   172  //
   173  // Args:
   174  //   - userID is the luci auth identity of the requestor.
   175  //   - requestID is the user's provided requestID.
   176  //
   177  // Example (`RequestDedupKey("user:user@example.com", "something")`):
   178  //
   179  //	"a~r~user:user@example.com~something
   180  //
   181  // Returns a request deduplication key.
   182  //
   183  // None of the arguments may contain "~".
   184  func RequestDedupKey(id *quotapb.RequestDedupKey) string {
   185  	return serializeRedisKey(requestDedupKeyPrefix, id)
   186  }