github.com/greenpau/go-authcrunch@v1.1.4/pkg/registry/cache.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     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 registry
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"sync"
    21  	"time"
    22  )
    23  
    24  const (
    25  	// The default registration cleanup interval is 5 minutes.
    26  	defaultRegistrationCleanupInternal int = 300
    27  	minRegistrationCleanupInternal     int = 0
    28  	// The default lifetime of a registration lifetime is 60 minutes.
    29  	defaultRegistrationMaxEntryLifetime int = 3600
    30  	// The minimum lifetime of a registration lifetime is 15 minutes.
    31  	minRegistrationMaxEntryLifetime int = 900
    32  )
    33  
    34  // RegistrationCacheEntry is an entry in RegistrationCache.
    35  type RegistrationCacheEntry struct {
    36  	registrationID string
    37  	createdAt      time.Time
    38  	user           map[string]string
    39  	// When set to true, the entry is no longer active.
    40  	expired bool
    41  }
    42  
    43  // RegistrationCache contains cached tokens
    44  type RegistrationCache struct {
    45  	mu sync.RWMutex
    46  	// The interval (in seconds) at which cache maintenance task are being triggered.
    47  	// The default is 5 minutes (300 seconds)
    48  	cleanupInternal int
    49  	// The maximum number of seconds the cached entry is available to a user.
    50  	maxEntryLifetime int
    51  	// If set to true, then the cache is being managed.
    52  	managed bool
    53  	// exit channel
    54  	exit    chan bool
    55  	Entries map[string]*RegistrationCacheEntry `json:"entries,omitempty" xml:"entries,omitempty" yaml:"entries,omitempty"`
    56  }
    57  
    58  // NewRegistrationCache returns RegistrationCache instance.
    59  func NewRegistrationCache() *RegistrationCache {
    60  	return &RegistrationCache{
    61  		cleanupInternal:  defaultRegistrationCleanupInternal,
    62  		maxEntryLifetime: defaultRegistrationMaxEntryLifetime,
    63  		Entries:          make(map[string]*RegistrationCacheEntry),
    64  		exit:             make(chan bool),
    65  	}
    66  }
    67  
    68  // SetCleanupInterval sets cache management interval.
    69  func (c *RegistrationCache) SetCleanupInterval(i int) error {
    70  	if i < 1 {
    71  		return fmt.Errorf("registration cache cleanup interval must be equal to or greater than %d", minRegistrationCleanupInternal)
    72  	}
    73  	c.cleanupInternal = i
    74  	return nil
    75  }
    76  
    77  // SetMaxEntryLifetime sets cache management max entry lifetime in seconds.
    78  func (c *RegistrationCache) SetMaxEntryLifetime(i int) error {
    79  	if i < 60 {
    80  		return fmt.Errorf("registration cache max entry lifetime must be equal to or greater than %d seconds", minRegistrationMaxEntryLifetime)
    81  	}
    82  	c.maxEntryLifetime = i
    83  	return nil
    84  }
    85  
    86  func manageRegistrationCache(c *RegistrationCache) {
    87  	c.managed = true
    88  	intervals := time.NewTicker(time.Second * time.Duration(c.cleanupInternal))
    89  	for range intervals.C {
    90  		if c == nil {
    91  			continue
    92  		}
    93  		c.mu.Lock()
    94  		select {
    95  		case <-c.exit:
    96  			c.managed = false
    97  			break
    98  		default:
    99  			break
   100  		}
   101  		if !c.managed {
   102  			c.mu.Unlock()
   103  			break
   104  		}
   105  		if c.Entries == nil {
   106  			c.mu.Unlock()
   107  			continue
   108  		}
   109  		deleteList := []string{}
   110  		for registrationID, entry := range c.Entries {
   111  			if err := entry.Valid(c.maxEntryLifetime); err != nil {
   112  				deleteList = append(deleteList, registrationID)
   113  				continue
   114  			}
   115  		}
   116  		if len(deleteList) > 0 {
   117  			for _, registrationID := range deleteList {
   118  				delete(c.Entries, registrationID)
   119  			}
   120  		}
   121  		c.mu.Unlock()
   122  	}
   123  	return
   124  }
   125  
   126  // Run starts management of RegistrationCache instance.
   127  func (c *RegistrationCache) Run() {
   128  	if c.managed {
   129  		return
   130  	}
   131  	go manageRegistrationCache(c)
   132  }
   133  
   134  // Stop stops management of RegistrationCache instance.
   135  func (c *RegistrationCache) Stop() {
   136  	c.mu.Lock()
   137  	defer c.mu.Unlock()
   138  	c.managed = false
   139  }
   140  
   141  // GetCleanupInterval returns cleanup interval.
   142  func (c *RegistrationCache) GetCleanupInterval() int {
   143  	return c.cleanupInternal
   144  }
   145  
   146  // GetMaxEntryLifetime returns max entry lifetime.
   147  func (c *RegistrationCache) GetMaxEntryLifetime() int {
   148  	return c.maxEntryLifetime
   149  }
   150  
   151  // Add adds user to the cache.
   152  func (c *RegistrationCache) Add(registrationID string, u map[string]string) error {
   153  	c.mu.Lock()
   154  	defer c.mu.Unlock()
   155  	if c.Entries == nil {
   156  		return errors.New("registration cache is not available")
   157  	}
   158  
   159  	for _, field := range []string{"username", "password", "email"} {
   160  		if _, exists := u[field]; !exists {
   161  			return fmt.Errorf("input entry has no %s field", field)
   162  		}
   163  	}
   164  
   165  	for _, m := range c.Entries {
   166  		if m.user == nil {
   167  			continue
   168  		}
   169  		for _, field := range []string{"username", "email"} {
   170  			if m.user[field] == u[field] {
   171  				return fmt.Errorf("a record with this %s already exists", field)
   172  			}
   173  		}
   174  	}
   175  
   176  	c.Entries[registrationID] = &RegistrationCacheEntry{
   177  		registrationID: registrationID,
   178  		createdAt:      time.Now().UTC(),
   179  		user:           u,
   180  	}
   181  	return nil
   182  }
   183  
   184  // Delete removes cached user entry.
   185  func (c *RegistrationCache) Delete(registrationID string) error {
   186  	c.mu.Lock()
   187  	defer c.mu.Unlock()
   188  	if c.Entries == nil {
   189  		return errors.New("registration cache is not available")
   190  	}
   191  	_, exists := c.Entries[registrationID]
   192  	if !exists {
   193  		return errors.New("cached registration id not found")
   194  	}
   195  	delete(c.Entries, registrationID)
   196  	return nil
   197  }
   198  
   199  // Get returns cached user entry.
   200  func (c *RegistrationCache) Get(registrationID string) (map[string]string, error) {
   201  	if err := parseCacheID(registrationID); err != nil {
   202  		return nil, err
   203  	}
   204  	c.mu.RLock()
   205  	defer c.mu.RUnlock()
   206  	if entry, exists := c.Entries[registrationID]; exists {
   207  		if err := entry.Valid(c.maxEntryLifetime); err != nil {
   208  			return nil, err
   209  		}
   210  		return entry.user, nil
   211  	}
   212  	return nil, errors.New("cached registration id not found")
   213  }
   214  
   215  // Expire expires a particular registration entry.
   216  func (c *RegistrationCache) Expire(registrationID string) {
   217  	c.mu.Lock()
   218  	defer c.mu.Unlock()
   219  	if entry, exists := c.Entries[registrationID]; exists {
   220  		entry.expired = true
   221  	}
   222  	return
   223  }
   224  
   225  // Valid checks whether RegistrationCacheEntry is non-expired.
   226  func (e *RegistrationCacheEntry) Valid(max int) error {
   227  	if e.expired {
   228  		return errors.New("registration cached entry is no longer in use")
   229  	}
   230  	diff := time.Now().UTC().Unix() - e.createdAt.Unix()
   231  	if diff > int64(max) {
   232  		return errors.New("registration cached entry expired")
   233  	}
   234  	return nil
   235  }
   236  
   237  // parseCacheID checks the id associated with the cached entry for format
   238  // requirements.
   239  func parseCacheID(s string) error {
   240  	if len(s) > 96 || len(s) < 32 {
   241  		return errors.New("cached id length is outside of 32-96 character range")
   242  	}
   243  	for _, c := range s {
   244  		if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && (c != '-') {
   245  			return errors.New("cached id contains invalid characters")
   246  		}
   247  	}
   248  	return nil
   249  }