forked from influxdata/kapacitor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
216 lines (191 loc) · 5.01 KB
/
auth.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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package auth
import (
"fmt"
"path"
"strings"
)
// Interface for authenticating and retrieving users.
type Interface interface {
Authenticate(username, password string) (User, error)
User(username string) (User, error)
SubscriptionUser(token string) (User, error)
GrantSubscriptionAccess(token, db, rp string) error
ListSubscriptionTokens() ([]string, error)
RevokeSubscriptionAccess(token string) error
}
type Privilege uint
const (
NoPrivileges Privilege = 1 << iota
ReadPrivilege
WritePrivilege
DeletePrivilege
AllPrivileges
)
var PrivilegeList []Privilege
func init() {
p := Privilege(0)
for i := uint(0); p < AllPrivileges; i++ {
p = 1 << i
PrivilegeList = append(PrivilegeList, p)
}
}
func (p Privilege) String() string {
switch p {
case NoPrivileges:
return "none"
case ReadPrivilege:
return "read"
case WritePrivilege:
return "write"
case DeletePrivilege:
return "delete"
case AllPrivileges:
return "all"
default:
return "unknown"
}
}
type Action struct {
Resource string
Privilege Privilege
}
// This structure is designed to be immutable, to avoid bugs/exploits where
// the user could be modified by external code.
// For this reason all fields are private and methods are value receivers.
type User struct {
name string
admin bool
hash []byte
// Map of resource -> Bitmask of Privileges
privileges map[string]Privilege
}
// Create a user with the given privileges.
func NewUser(name string, hash []byte, admin bool, privileges map[string][]Privilege) User {
ps := make(map[string]Privilege, len(privileges))
// Clean resources and convert to bitmask
for resource, privileges := range privileges {
clean := path.Clean(resource)
mask := Privilege(0)
for _, p := range privileges {
mask |= p
}
ps[clean] = mask
}
// Make our own copy of the hash
h := make([]byte, len(hash))
copy(h, hash)
return User{
name: name,
admin: admin,
hash: h,
privileges: ps,
}
}
// This user has all privileges for all resources.
var AdminUser = NewUser("ADMIN_USER", nil, true, nil)
// Determine wether the user is authorized to take the action.
func (u User) Name() string {
return u.name
}
// Report whether the user is an Admin user
func (u User) IsAdmin() bool {
return u.admin
}
// Return a copy of the user's password hash
func (u User) Hash() []byte {
hash := make([]byte, len(u.hash))
copy(hash, u.hash)
return hash
}
// Return a copy of the privileges the user has.
func (u User) Privileges() map[string][]Privilege {
privileges := make(map[string][]Privilege)
for r, ps := range u.privileges {
for _, p := range PrivilegeList {
if ps&p != 0 {
privileges[r] = append(privileges[r], p)
}
}
}
return privileges
}
// Determine wether the user is authorized to take the action.
// Returns nil if the action is authorized, otherwise returns an error describing the needed permissions.
func (u User) AuthorizeAction(action Action) error {
if action.Privilege == NoPrivileges || u.admin {
return nil
}
// Find a matching resource of the form /path/to/resource
// where if the resource is /a/b/c and the user has permision to /a/b
// then it is considered valid.
if !path.IsAbs(action.Resource) {
return fmt.Errorf("invalid action resource: %q, must be an absolute path", action.Resource)
}
if len(u.privileges) > 0 {
// Clean path to prevent path traversal like /a/b/../d when user has access to /a/b
resource := path.Clean(action.Resource)
for {
if p, ok := u.privileges[resource]; ok {
// Found matching resource
authorized := p&action.Privilege != 0 || p == AllPrivileges
if authorized {
return nil
} else {
break
}
}
if resource == "/" {
break
}
// Pop off the last piece of the resource and try again
resource = path.Dir(resource)
}
}
return authError{
username: u.name,
action: action,
}
}
// All auth errors are of this type.
type authError struct {
username string
action Action
}
func (e authError) Error() string {
return fmt.Sprintf("user %s does not have \"%v\" privilege for resource %q", e.username, e.action.Privilege, e.action.Resource)
}
func (e authError) MissingPrivlege() Privilege {
return e.action.Privilege
}
const (
databaseRootResource = "/database"
apiRootResource = "/api"
cleanSuffix = "_clean"
dirtySuffix = "_dirty"
)
func APIResource(p string) string {
return path.Join(apiRootResource, p)
}
func DatabaseResource(database string) string {
// Map the database name to a path element name.
// 1. Replace all '/' with '_'.
// 2. Mark whether the database name was clean or dirty.
//
// This transformation has two important properties:
// 1. The resulting name will be considered a single path element.
// 2. The transformation is a one-to-one function.
//
// Examples:
// db/name -> db_name_dirty
// db_name -> db_name_clean
if database == "" {
return databaseRootResource
}
db := strings.Replace(database, "/", "_", -1)
if db == database {
db += cleanSuffix
} else {
db += dirtySuffix
}
return path.Join(databaseRootResource, db)
}