-
Notifications
You must be signed in to change notification settings - Fork 7
/
accessControlKeys.go
136 lines (118 loc) · 3.33 KB
/
accessControlKeys.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package main
import (
"database/sql"
"math/rand"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/lib/pq"
"github.com/unioslo/nivlheim/server/service/utility"
)
type APIkey string
type apiKeyCacheElem struct {
ap *AccessProfile
created time.Time
}
var apiKeyCacheMutex sync.RWMutex
var apiKeyCache map[string]apiKeyCacheElem
func init() {
apiKeyCache = make(map[string]apiKeyCacheElem, 0)
}
func GetAPIKeyFromRequest(req *http.Request) APIkey {
auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
if len(auth) == 2 && strings.ToLower(auth[0]) == "apikey" {
return APIkey(auth[1])
}
return ""
}
func GenerateTemporaryAPIKey(ap *AccessProfile) APIkey {
apiKeyCacheMutex.Lock()
defer apiKeyCacheMutex.Unlock()
key := utility.RandomStringID()
apiKeyCache[key] = apiKeyCacheElem{ap: ap, created: time.Now()}
return APIkey(key)
}
func GetAccessProfileForAPIkey(key APIkey, db *sql.DB, existingUserAP *AccessProfile) (*AccessProfile, error) {
const cacheTimeMinutes = 10
// 1. Check the cache
apiKeyCacheMutex.RLock()
c, ok := apiKeyCache[string(key)]
apiKeyCacheMutex.RUnlock()
if ok && time.Since(c.created) < time.Duration(cacheTimeMinutes)*time.Minute {
return c.ap, nil
}
// 2. Read the entry from the database table
var keyID int
var expires pq.NullTime
var readonly, allGroups sql.NullBool
var groups []string
err := db.QueryRow("SELECT keyid, groups, expires, readonly, all_groups "+
"FROM apikeys WHERE key=$1", string(key)).
Scan(&keyID, pq.Array(&groups), &expires, &readonly, &allGroups)
if err != nil {
if err != sql.ErrNoRows {
return nil, err
}
return nil, nil // No key was found, but this isn't an error
}
// 3. Some of the test scripts supply an AccessProfile to create various testing scenarios.
// In production, existingUserAP is always nil.
var ap *AccessProfile
if existingUserAP != nil {
ap = existingUserAP
} else {
ap = new(AccessProfile)
}
// 4. Set various fields in the struct
ap.readonly = readonly.Bool
ap.isAdmin = false // keys can't give you admin rights. This may change in the future.
ap.allGroups = allGroups.Bool
if expires.Valid {
ap.expires = expires.Time
}
ap.groups = make(map[string]bool, len(groups))
for _, g := range groups {
ap.groups[g] = true
}
// 5. Get the IP ranges
rows, err := db.Query("SELECT iprange FROM apikey_ips WHERE keyID=$1", keyID)
if err != nil {
return nil, err
}
defer rows.Close()
ap.ipranges = make([]net.IPNet, 0)
for rows.Next() {
var r string
err = rows.Scan(&r)
if err != nil {
return nil, err
}
_, ipnet, err := net.ParseCIDR(r)
if err != nil {
return nil, err
}
ap.ipranges = append(ap.ipranges, *ipnet)
}
rows.Close()
// 6. Cache the AccessProfile, so that subsequent calls to GetAccessProfileForAPIkey can quickly use it.
apiKeyCacheMutex.Lock()
defer apiKeyCacheMutex.Unlock()
apiKeyCache[string(key)] = apiKeyCacheElem{ap: ap, created: time.Now()}
// 7. Purge expired keys from the cache sometimes
if rand.Intn(100) == 0 {
// the mutex is already locked, so it's safe to modify the map
for id, c := range apiKeyCache {
if time.Since(c.created) > time.Duration(cacheTimeMinutes)*time.Minute {
delete(apiKeyCache, id)
}
}
}
return ap, nil
}
func invalidateCacheForKey(key string) {
apiKeyCacheMutex.Lock()
defer apiKeyCacheMutex.Unlock()
delete(apiKeyCache, key)
}