Commit 1769d6d42e5abc1a6a9f997088da1e8eb2a51fff

Authored by Marko Tikvić
1 parent d2ddf82ef1
Exists in master and in 1 other branch v2

rbac

Showing 1 changed file with 2 additions and 2 deletions   Show diff stats
1 // TODO: Improve roles 1 // TODO: Improve roles
2 package webutility 2 package webutility
3 3
4 import ( 4 import (
5 "crypto/rand" 5 "crypto/rand"
6 "crypto/sha256" 6 "crypto/sha256"
7 "encoding/hex" 7 "encoding/hex"
8 "errors" 8 "errors"
9 "net/http" 9 "net/http"
10 "strings" 10 "strings"
11 "time" 11 "time"
12 12
13 "github.com/dgrijalva/jwt-go" 13 "github.com/dgrijalva/jwt-go"
14 ) 14 )
15 15
16 const OneDay = time.Hour * 24 16 const OneDay = time.Hour * 24
17 const OneWeek = OneDay * 7 17 const OneWeek = OneDay * 7
18 const saltSize = 32 18 const saltSize = 32
19 const appName = "korisnicki-centar" 19 const appName = "korisnicki-centar"
20 const secret = "korisnicki-centar-api" 20 const secret = "korisnicki-centar-api"
21 21
22 const RoleAdmin string = "ADMINISTRATOR" 22 const RoleAdmin string = "ADMINISTRATOR"
23 const RoleManager string = "RUKOVODILAC" 23 const RoleManager string = "RUKOVODILAC"
24 const RoleReporter string = "REPORTER" 24 const RoleReporter string = "REPORTER"
25 const RoleOperator string = "OPERATER" 25 const RoleOperator string = "OPERATER"
26 const RoleAdminID uint32 = 1 26 const RoleAdminID uint32 = 1
27 const RoleManagerID uint32 = 2 27 const RoleManagerID uint32 = 2
28 const RoleReporterID uint32 = 3 28 const RoleReporterID uint32 = 3
29 const RoleOperatorID uint32 = 4 29 const RoleOperatorID uint32 = 4
30 30
31 type Role struct { 31 type Role struct {
32 name string 32 name string
33 id uint32 33 id uint32
34 } 34 }
35 35
36 // TokenClaims are JWT token claims. 36 // TokenClaims are JWT token claims.
37 type TokenClaims struct { 37 type TokenClaims struct {
38 Username string `json:"username"` 38 Username string `json:"username"`
39 Role string `json:"role"` 39 Role string `json:"role"`
40 RoleID uint32 `json:"roleID"` 40 RoleID uint32 `json:"roleID"`
41 jwt.StandardClaims 41 jwt.StandardClaims
42 } 42 }
43 43
44 // CredentialsStruct is an instace of username/password values. 44 // CredentialsStruct is an instace of username/password values.
45 type CredentialsStruct struct { 45 type CredentialsStruct struct {
46 Username string `json:"username"` 46 Username string `json:"username"`
47 Password string `json:"password"` 47 Password string `json:"password"`
48 } 48 }
49 49
50 var admin Role = Role{RoleAdmin, RoleAdminID} 50 var admin Role = Role{RoleAdmin, RoleAdminID}
51 var manager Role = Role{RoleManager, RoleManagerID} 51 var manager Role = Role{RoleManager, RoleManagerID}
52 var reporter Role = Role{RoleReporter, RoleReporterID} 52 var reporter Role = Role{RoleReporter, RoleReporterID}
53 var operator Role = Role{RoleOperator, RoleOperatorID} 53 var operator Role = Role{RoleOperator, RoleOperatorID}
54 54
55 // generateSalt returns a string of random characters of 'saltSize' length. 55 // generateSalt returns a string of random characters of 'saltSize' length.
56 func generateSalt() (salt string, err error) { 56 func generateSalt() (salt string, err error) {
57 rawsalt := make([]byte, saltSize) 57 rawsalt := make([]byte, saltSize)
58 58
59 _, err = rand.Read(rawsalt) 59 _, err = rand.Read(rawsalt)
60 if err != nil { 60 if err != nil {
61 return "", err 61 return "", err
62 } 62 }
63 63
64 salt = hex.EncodeToString(rawsalt) 64 salt = hex.EncodeToString(rawsalt)
65 return salt, nil 65 return salt, nil
66 } 66 }
67 67
68 // HashString hashes input string with SHA256 algorithm. 68 // HashString hashes input string with SHA256 algorithm.
69 // If the presalt parameter is not provided HashString will generate new salt string. 69 // If the presalt parameter is not provided HashString will generate new salt string.
70 // Returns hash and salt string or an error if it fails. 70 // Returns hash and salt string or an error if it fails.
71 func HashString(str string, presalt string) (hash, salt string, err error) { 71 func HashString(str string, presalt string) (hash, salt string, err error) {
72 // chech if message is presalted 72 // chech if message is presalted
73 if presalt == "" { 73 if presalt == "" {
74 salt, err = generateSalt() 74 salt, err = generateSalt()
75 if err != nil { 75 if err != nil {
76 return "", "", err 76 return "", "", err
77 } 77 }
78 } else { 78 } else {
79 salt = presalt 79 salt = presalt
80 } 80 }
81 81
82 // convert strings to raw byte slices 82 // convert strings to raw byte slices
83 rawstr := []byte(str) 83 rawstr := []byte(str)
84 rawsalt, err := hex.DecodeString(salt) 84 rawsalt, err := hex.DecodeString(salt)
85 if err != nil { 85 if err != nil {
86 return "", "", err 86 return "", "", err
87 } 87 }
88 88
89 rawdata := make([]byte, len(rawstr)+len(rawsalt)) 89 rawdata := make([]byte, len(rawstr)+len(rawsalt))
90 rawdata = append(rawdata, rawstr...) 90 rawdata = append(rawdata, rawstr...)
91 rawdata = append(rawdata, rawsalt...) 91 rawdata = append(rawdata, rawsalt...)
92 92
93 // hash message + salt 93 // hash message + salt
94 hasher := sha256.New() 94 hasher := sha256.New()
95 hasher.Write(rawdata) 95 hasher.Write(rawdata)
96 rawhash := hasher.Sum(nil) 96 rawhash := hasher.Sum(nil)
97 97
98 hash = hex.EncodeToString(rawhash) 98 hash = hex.EncodeToString(rawhash)
99 return hash, salt, nil 99 return hash, salt, nil
100 } 100 }
101 101
102 // CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims. 102 // CreateAPIToken returns JWT token with encoded username, role, expiration date and issuer claims.
103 // It returns an error if it fails. 103 // It returns an error if it fails.
104 func CreateAPIToken(username, role string, roleID uint32) (string, error) { 104 func CreateAPIToken(username, role string, roleID uint32) (string, error) {
105 var apiToken string 105 var apiToken string
106 var err error 106 var err error
107 107
108 if err != nil { 108 if err != nil {
109 return "", err 109 return "", err
110 } 110 }
111 111
112 claims := TokenClaims{ 112 claims := TokenClaims{
113 username, 113 username,
114 role, 114 role,
115 roleID, 115 roleID,
116 jwt.StandardClaims{ 116 jwt.StandardClaims{
117 ExpiresAt: (time.Now().Add(OneWeek)).Unix(), 117 ExpiresAt: (time.Now().Add(OneWeek)).Unix(),
118 Issuer: appName, 118 Issuer: appName,
119 }, 119 },
120 } 120 }
121 121
122 jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 122 jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
123 apiToken, err = jwtToken.SignedString([]byte(secret)) 123 apiToken, err = jwtToken.SignedString([]byte(secret))
124 if err != nil { 124 if err != nil {
125 return "", err 125 return "", err
126 } 126 }
127 return apiToken, nil 127 return apiToken, nil
128 } 128 }
129 129
130 // RefreshAPIToken prolongs JWT token's expiration date for one week. 130 // RefreshAPIToken prolongs JWT token's expiration date for one week.
131 // It returns new JWT token or an error if it fails. 131 // It returns new JWT token or an error if it fails.
132 func RefreshAPIToken(tokenString string) (string, error) { 132 func RefreshAPIToken(tokenString string) (string, error) {
133 var newToken string 133 var newToken string
134 tokenString = strings.TrimPrefix(tokenString, "Bearer ") 134 tokenString = strings.TrimPrefix(tokenString, "Bearer ")
135 token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) 135 token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc)
136 if err != nil { 136 if err != nil {
137 return "", err 137 return "", err
138 } 138 }
139 139
140 // type assertion 140 // type assertion
141 claims, ok := token.Claims.(*TokenClaims) 141 claims, ok := token.Claims.(*TokenClaims)
142 if !ok || !token.Valid { 142 if !ok || !token.Valid {
143 return "", errors.New("token is not valid") 143 return "", errors.New("token is not valid")
144 } 144 }
145 145
146 claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix() 146 claims.ExpiresAt = (time.Now().Add(OneWeek)).Unix()
147 jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 147 jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
148 148
149 newToken, err = jwtToken.SignedString([]byte(secret)) 149 newToken, err = jwtToken.SignedString([]byte(secret))
150 if err != nil { 150 if err != nil {
151 return "", err 151 return "", err
152 } 152 }
153 153
154 return newToken, nil 154 return newToken, nil
155 } 155 }
156 156
157 // ParseAPIToken parses JWT token claims. 157 // ParseAPIToken parses JWT token claims.
158 // It returns a pointer to TokenClaims struct or an error if it fails. 158 // It returns a pointer to TokenClaims struct or an error if it fails.
159 func ParseAPIToken(tokenString string) (*TokenClaims, error) { 159 func ParseAPIToken(tokenString string) (*TokenClaims, error) {
160 if ok := strings.HasPrefix(tokenString, "Bearer "); ok { 160 if ok := strings.HasPrefix(tokenString, "Bearer "); ok {
161 tokenString = strings.TrimPrefix(tokenString, "Bearer ") 161 tokenString = strings.TrimPrefix(tokenString, "Bearer ")
162 } else { 162 } else {
163 return &TokenClaims{}, errors.New("Authorization header is incomplete") 163 return &TokenClaims{}, errors.New("Authorization header is incomplete")
164 } 164 }
165 165
166 token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc) 166 token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, secretFunc)
167 if err != nil { 167 if err != nil {
168 return &TokenClaims{}, err 168 return &TokenClaims{}, err
169 } 169 }
170 170
171 // type assertion 171 // type assertion
172 claims, ok := token.Claims.(*TokenClaims) 172 claims, ok := token.Claims.(*TokenClaims)
173 if !ok || !token.Valid { 173 if !ok || !token.Valid {
174 return &TokenClaims{}, errors.New("token is not valid") 174 return &TokenClaims{}, errors.New("token is not valid")
175 } 175 }
176 return claims, nil 176 return claims, nil
177 } 177 }
178 178
179 func GetTokenClaims(r *http.Request) (claims *TokenClaims, err error) { 179 func GetTokenClaims(r *http.Request) (claims *TokenClaims, err error) {
180 token := r.Header.Get("Authorization") 180 token := r.Header.Get("Authorization")
181 if ok := strings.HasPrefix(token, "Bearer "); ok { 181 if ok := strings.HasPrefix(token, "Bearer "); ok {
182 token = strings.TrimPrefix(token, "Bearer ") 182 token = strings.TrimPrefix(token, "Bearer ")
183 } else { 183 } else {
184 return &TokenClaims{}, errors.New("Authorization header is incomplete") 184 return &TokenClaims{}, errors.New("Authorization header is incomplete")
185 } 185 }
186 186
187 parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc) 187 parsedToken, err := jwt.ParseWithClaims(token, &TokenClaims{}, secretFunc)
188 if err != nil { 188 if err != nil {
189 return &TokenClaims{}, err 189 return &TokenClaims{}, err
190 } 190 }
191 191
192 // type assertion 192 // type assertion
193 claims, ok := parsedToken.Claims.(*TokenClaims) 193 claims, ok := parsedToken.Claims.(*TokenClaims)
194 if !ok || !parsedToken.Valid { 194 if !ok || !parsedToken.Valid {
195 return &TokenClaims{}, errors.New("token is not valid") 195 return &TokenClaims{}, errors.New("token is not valid")
196 } 196 }
197 return claims, err 197 return claims, err
198 } 198 }
199 199
200 // secretFunc returns byte slice of API secret keyword. 200 // secretFunc returns byte slice of API secret keyword.
201 func secretFunc(token *jwt.Token) (interface{}, error) { 201 func secretFunc(token *jwt.Token) (interface{}, error) {
202 return []byte(secret), nil 202 return []byte(secret), nil
203 } 203 }
204 204
205 // rbacEnforce returns true if role that made HTTP request is authorized to 205 // RbacCheck returns true if role that made HTTP request is authorized to
206 // access the resource it is targeting. 206 // access the resource it is targeting.
207 // It exctracts user's role from the JWT token located in Authorization header of 207 // It exctracts user's role from the JWT token located in Authorization header of
208 // http.Request and then compares it with the list of supplied roles and returns 208 // http.Request and then compares it with the list of supplied roles and returns
209 // true if there's a match, if "*" is provided or if the authRoles is nil. 209 // true if there's a match, if "*" is provided or if the authRoles is nil.
210 // Otherwise it returns false. 210 // Otherwise it returns false.
211 func RbacCheck(req *http.Request, authRoles []string) bool { 211 func RbacCheck(req *http.Request, authRoles []string) bool {
212 if authRoles == nil { 212 if authRoles == nil {
213 return true 213 return true
214 } 214 }
215 215
216 token := req.Header.Get("Authorization") 216 token := req.Header.Get("Authorization")
217 claims, err := ParseAPIToken(token) 217 claims, err := ParseAPIToken(token)
218 if err != nil { 218 if err != nil {
219 return false 219 return false
220 } 220 }
221 221
222 for _, r := range authRoles { 222 for _, r := range authRoles {
223 if claims.Role == r || r == "*" { 223 if claims.Role == r || r == "*" {
224 return true 224 return true
225 } 225 }
226 } 226 }
227 227
228 return false 228 return false
229 } 229 }
230 230
231 // Rbac sets common headers and performs RBAC. 231 // Rbac sets common headers and performs RBAC.
232 // If RBAC passes it calls the handlerFunc. 232 // If RBAC passes it calls the handlerFunc.
233 func Rbac(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc { 233 func RbacHandler(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc {
234 return func(w http.ResponseWriter, req *http.Request) { 234 return func(w http.ResponseWriter, req *http.Request) {
235 w.Header().Set("Access-Control-Allow-Origin", "*") 235 w.Header().Set("Access-Control-Allow-Origin", "*")
236 236
237 w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") 237 w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
238 238
239 w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, 239 w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type,
240 Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) 240 Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`)
241 241
242 w.Header().Set("Content-Type", "application/json; charset=utf-8") 242 w.Header().Set("Content-Type", "application/json; charset=utf-8")
243 243
244 // TODO: Check for content type 244 // TODO: Check for content type
245 245
246 if req.Method == "OPTIONS" { 246 if req.Method == "OPTIONS" {
247 return 247 return
248 } 248 }
249 249
250 err := req.ParseForm() 250 err := req.ParseForm()
251 if err != nil { 251 if err != nil {
252 BadRequestResponse(w, req) 252 BadRequestResponse(w, req)
253 return 253 return
254 } 254 }
255 255
256 if !RbacCheck(req, authRoles) { 256 if !RbacCheck(req, authRoles) {
257 UnauthorizedResponse(w, req) 257 UnauthorizedResponse(w, req)
258 return 258 return
259 } 259 }
260 260
261 // execute HandlerFunc 261 // execute HandlerFunc
262 handlerFunc(w, req) 262 handlerFunc(w, req)
263 } 263 }
264 } 264 }
265 265