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 := "apb.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 := "apb.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 := "apb.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 = "apb.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 "apb.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 := "apb.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 }