github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }