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 }