github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/features.go (about) 1 package libkb 2 3 import ( 4 "strings" 5 "sync" 6 "time" 7 8 keybase1 "github.com/keybase/client/go/protocol/keybase1" 9 ) 10 11 type Feature string 12 type FeatureFlags []Feature 13 14 const ( 15 EnvironmentFeatureAllowHighSkips = Feature("env_allow_high_skips") 16 EnvironmentFeatureMerkleCheckpoint = Feature("merkle_checkpoint") 17 ) 18 19 // StringToFeatureFlags returns a set of feature flags 20 func StringToFeatureFlags(s string) (ret FeatureFlags) { 21 s = strings.TrimSpace(s) 22 if len(s) == 0 { 23 return ret 24 } 25 v := strings.Split(s, ",") 26 for _, f := range v { 27 ret = append(ret, Feature(strings.TrimSpace(f))) 28 } 29 return ret 30 } 31 32 // Admin returns true if the admin feature set is on or the user is a keybase 33 // admin. 34 func (set FeatureFlags) Admin(uid keybase1.UID) bool { 35 for _, f := range set { 36 if f == Feature("admin") { 37 return true 38 } 39 } 40 return IsKeybaseAdmin(uid) 41 } 42 43 func (set FeatureFlags) HasFeature(feature Feature) bool { 44 for _, f := range set { 45 if f == feature { 46 return true 47 } 48 } 49 return false 50 } 51 52 func (set FeatureFlags) Empty() bool { 53 return len(set) == 0 54 } 55 56 type featureSlot struct { 57 on bool 58 cacheUntil time.Time 59 } 60 61 // FeatureFlagSet is a set of feature flags for a given user. It will keep track 62 // of whether a feature is on or off, and how long until we should check to 63 // update 64 type FeatureFlagSet struct { 65 sync.RWMutex 66 features map[Feature]*featureSlot 67 } 68 69 const ( 70 FeatureBoxAuditor = Feature("box_auditor3") 71 ExperimentalGenericProofs = Feature("experimental_generic_proofs") 72 FeatureCheckForHiddenChainSupport = Feature("check_for_hidden_chain_support") 73 74 // Show journeycards. This 'preview' flag is for development and admin testing. 75 // This 'preview' flag is known to clients with old buggy journeycard code. For that reason, don't enable it for external users. 76 FeatureJourneycardPreview = Feature("journeycard_preview") 77 FeatureJourneycard = Feature("journeycard") 78 ) 79 80 // getInitialFeatures returns the features which a new FeatureFlagSet should 81 // contain so that they are prefetched the first time the set is used. 82 func getInitialFeatures() []Feature { 83 return []Feature{ 84 FeatureBoxAuditor, 85 ExperimentalGenericProofs, 86 FeatureCheckForHiddenChainSupport, 87 FeatureJourneycardPreview, 88 FeatureJourneycard} 89 } 90 91 // NewFeatureFlagSet makes a new set of feature flags. 92 func NewFeatureFlagSet() *FeatureFlagSet { 93 features := make(map[Feature]*featureSlot) 94 for _, f := range getInitialFeatures() { 95 features[f] = &featureSlot{} 96 } 97 return &FeatureFlagSet{features: features} 98 } 99 100 type rawFeatureSlot struct { 101 Value bool `json:"value"` 102 CacheSec int `json:"cache_sec"` 103 } 104 105 type rawFeatures struct { 106 Status AppStatus `json:"status"` 107 Features map[string]rawFeatureSlot `json:"features"` 108 } 109 110 func (r *rawFeatures) GetAppStatus() *AppStatus { 111 return &r.Status 112 } 113 114 func (f *featureSlot) readFrom(m MetaContext, r rawFeatureSlot) { 115 f.on = r.Value 116 f.cacheUntil = m.G().Clock().Now().Add(time.Duration(r.CacheSec) * time.Second) 117 } 118 119 func (s *FeatureFlagSet) InvalidateCache(m MetaContext, f Feature) { 120 s.Lock() 121 defer s.Unlock() 122 slot, found := s.features[f] 123 if !found { 124 return 125 } 126 slot.cacheUntil = m.G().Clock().Now().Add(time.Duration(-1) * time.Second) 127 } 128 129 func (s *FeatureFlagSet) refreshAllLocked(m MetaContext) (err error) { 130 // collect all feature names in the set, regardless of state 131 var features []string 132 for f := range s.features { 133 features = append(features, string(f)) 134 } 135 136 var raw rawFeatures 137 arg := NewAPIArg("user/features") 138 arg.SessionType = APISessionTypeREQUIRED 139 arg.Args = HTTPArgs{ 140 "features": S{Val: strings.Join(features, ",")}, 141 } 142 err = m.G().API.GetDecode(m, arg, &raw) 143 switch err.(type) { 144 case nil: 145 case LoginRequiredError: 146 // No features for logged-out users 147 return nil 148 default: 149 return err 150 } 151 152 for f, slot := range s.features { 153 rawFeature, ok := raw.Features[string(f)] 154 if !ok { 155 m.Debug("Feature %q wasn't returned from server, not updating", f) 156 continue 157 } 158 slot.readFrom(m, rawFeature) 159 m.Debug("Feature (fetched) %q -> %v (will cache for %ds)", f, slot.on, rawFeature.CacheSec) 160 } 161 return nil 162 } 163 164 // enabledInCacheRLocked must be called while holding (at least) the read lock on s 165 func (s *FeatureFlagSet) enabledInCacheRLocked(m MetaContext, f Feature) (on bool, found bool) { 166 slot, found := s.features[f] 167 if !found { 168 return false, false 169 } 170 if m.G().Clock().Now().Before(slot.cacheUntil) { 171 m.G().GetVDebugLog().CLogf(m.Ctx(), VLog1, "Feature (cached) %q -> %v", f, slot.on) 172 return slot.on, true 173 } 174 return false, false 175 } 176 177 // EnabledWithError returns if the given feature is enabled, it will return true if it's 178 // enabled, and an error if one occurred. 179 func (s *FeatureFlagSet) EnabledWithError(m MetaContext, f Feature) (on bool, err error) { 180 m = m.WithLogTag("FEAT") 181 182 s.RLock() 183 if on, found := s.enabledInCacheRLocked(m, f); found { 184 s.RUnlock() 185 return on, nil 186 } 187 s.RUnlock() 188 189 // cache did not help, we need to lock for writing and update 190 s.Lock() 191 defer s.Unlock() 192 // while we were waiting for the write lock, other threads might have already 193 // updated this, check again 194 if on, found := s.enabledInCacheRLocked(m, f); found { 195 return on, nil 196 } 197 198 if _, found := s.features[f]; !found { 199 s.features[f] = &featureSlot{} 200 } 201 err = s.refreshAllLocked(m) 202 if err != nil { 203 return false, err 204 } 205 return s.features[f].on, nil 206 } 207 208 // Enabled returns if the feature flag is enabled. It ignore errors and just acts 209 // as if the feature is off. 210 func (s *FeatureFlagSet) Enabled(m MetaContext, f Feature) (on bool) { 211 on, err := s.EnabledWithError(m, f) 212 if err != nil { 213 m.Debug("Error checking feature %q: %v", f, err) 214 return false 215 } 216 return on 217 } 218 219 // Clear clears out the cached feature flags, for instance if the user 220 // is going to logout. 221 func (s *FeatureFlagSet) Clear() { 222 s.Lock() 223 defer s.Unlock() 224 s.features = make(map[Feature]*featureSlot) 225 } 226 227 // FeatureFlagGate allows the server to disable certain features by replying with a 228 // FEATURE_FLAG API status code, which is then translated into a FeatureFlagError. 229 // We cache these errors for a given amount of time, so we're not spamming the 230 // same attempt over and over again. 231 type FeatureFlagGate struct { 232 sync.Mutex 233 lastCheck time.Time 234 lastError error 235 feature Feature 236 cacheFor time.Duration 237 } 238 239 // NewFeatureFlagGate makes a gate for the given feature that will cache for the given 240 // duration. 241 func NewFeatureFlagGate(f Feature, d time.Duration) *FeatureFlagGate { 242 return &FeatureFlagGate{ 243 feature: f, 244 cacheFor: d, 245 } 246 } 247 248 // DigestError should be called on the result of an API call. It will allow this gate 249 // to digest the error and maybe set up its internal caching for when to retry this 250 // feature. 251 func (f *FeatureFlagGate) DigestError(m MetaContext, err error) { 252 if err == nil { 253 return 254 } 255 ffe, ok := err.(FeatureFlagError) 256 if !ok { 257 return 258 } 259 if ffe.Feature() != f.feature { 260 m.Debug("Got feature flag error for wrong feature: %v", err) 261 return 262 } 263 264 m.Debug("Server reports feature %q is flagged off", f.feature) 265 266 f.Lock() 267 defer f.Unlock() 268 f.lastCheck = m.G().Clock().Now() 269 f.lastError = err 270 } 271 272 // ErrorIfFlagged should be called to avoid a feature if it's recently 273 // been feature-flagged "off" by the server. In that case, it will return 274 // the error that was originally returned by the server. 275 func (f *FeatureFlagGate) ErrorIfFlagged(m MetaContext) (err error) { 276 f.Lock() 277 defer f.Unlock() 278 if f.lastError == nil { 279 return nil 280 } 281 diff := m.G().Clock().Now().Sub(f.lastCheck) 282 if diff > f.cacheFor { 283 m.Debug("Feature flag %q expired %d ago, let's give it another try", f.feature, diff) 284 f.lastError = nil 285 f.lastCheck = time.Time{} 286 } 287 return f.lastError 288 } 289 290 func (f *FeatureFlagGate) Clear() { 291 f.Lock() 292 defer f.Unlock() 293 f.lastError = nil 294 }