diff --git a/auth_utility.go b/auth_utility.go index 5fedb16..c0a35e5 100644 --- a/auth_utility.go +++ b/auth_utility.go @@ -1,32 +1,38 @@ +// TODO: Improve roles package webutility import ( - "errors" - "time" - "crypto/sha256" "crypto/rand" + "crypto/sha256" "encoding/hex" - "strings" + "errors" "net/http" + "strings" + "time" "github.com/dgrijalva/jwt-go" ) -const OneDay = time.Hour*24 -const OneWeek = OneDay*7 +const OneDay = time.Hour * 24 +const OneWeek = OneDay * 7 const saltSize = 32 -const appName = "korisnicki-centar" -const secret = "korisnicki-centar-api" +const appName = "korisnicki-centar" +const secret = "korisnicki-centar-api" -const RoleAdmin string = "ADMINISTRATOR" -const RoleManager string = "RUKOVODILAC" +const RoleAdmin string = "ADMINISTRATOR" +const RoleManager string = "RUKOVODILAC" const RoleReporter string = "REPORTER" const RoleOperator string = "OPERATER" -const RoleAdminID uint32 = 1 -const RoleManagerID uint32 = 2 +const RoleAdminID uint32 = 1 +const RoleManagerID uint32 = 2 const RoleReporterID uint32 = 3 const RoleOperatorID uint32 = 4 +type Role struct { + name string + id uint32 +} + // TokenClaims are JWT token claims. type TokenClaims struct { Username string `json:"username"` @@ -41,6 +47,11 @@ type CredentialsStruct struct { Password string `json:"password"` } +var admin Role = Role{RoleAdmin, RoleAdminID} +var manager Role = Role{RoleManager, RoleManagerID} +var reporter Role = Role{RoleReporter, RoleReporterID} +var operator Role = Role{RoleOperator, RoleOperatorID} + // generateSalt returns a string of random characters of 'saltSize' length. func generateSalt() (salt string, err error) { rawsalt := make([]byte, saltSize) @@ -75,7 +86,7 @@ func HashString(str string, presalt string) (hash, salt string, err error) { return "", "", err } - rawdata := make([]byte, len(rawstr) + len(rawsalt)) + rawdata := make([]byte, len(rawstr)+len(rawsalt)) rawdata = append(rawdata, rawstr...) rawdata = append(rawdata, rawsalt...) @@ -191,16 +202,63 @@ func secretFunc(token *jwt.Token) (interface{}, error) { return []byte(secret), nil } -// roleAuthorized returns true if role from userClaims matches any of the authorizedRoles -// or if authorizedRoles contains "*". -func roleAuthorized(authorizedRoles []string, userClaims *TokenClaims) bool { - if userClaims == nil { +// rbacEnforce returns true if role that made HTTP request is authorized to +// access the resource it is targeting. +// It exctracts user's role from the JWT token located in Authorization header of +// http.Request and then compares it with the list of supplied roles and returns +// true if there's a match, if "*" is provided or if the authRoles is nil. +// Otherwise it returns false. +func RbacCheck(req *http.Request, authRoles []string) bool { + if authRoles == nil { + return true + } + + token := req.Header.Get("Authorization") + claims, err := ParseAPIToken(token) + if err != nil { return false } - for _, r := range authorizedRoles { - if userClaims.Role == r || r == "*" { + + for _, r := range authRoles { + if claims.Role == r || r == "*" { return true } } + return false } + +// Rbac sets common headers and performs RBAC. +// If RBAC passes it calls the handlerFunc. +func Rbac(handlerFunc http.HandlerFunc, authRoles []string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") + + w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, + Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // TODO: Check for content type + + if req.Method == "OPTIONS" { + return + } + + err := req.ParseForm() + if err != nil { + BadRequestResponse(w, req) + return + } + + if !RbacCheck(req, authRoles) { + UnauthorizedResponse(w, req) + return + } + + // execute HandlerFunc + handlerFunc(w, req) + } +} diff --git a/http_utility.go b/http_utility.go index ca90b63..2fc3b2d 100644 --- a/http_utility.go +++ b/http_utility.go @@ -1,8 +1,8 @@ package webutility import ( - "net/http" "encoding/json" + "net/http" ) const templateHttpErr500_EN = "An internal server error has occurred." @@ -24,7 +24,7 @@ type HttpErrorDesc struct { // ErrorResponse writes HTTP error to w. func ErrorResponse(w http.ResponseWriter, r *http.Request, code int, desc []HttpErrorDesc) { - err := httpError{ desc, r.Method + " " + r.URL.Path } + err := httpError{desc, r.Method + " " + r.URL.Path} w.WriteHeader(code) json.NewEncoder(w).Encode(err) } @@ -32,71 +32,46 @@ func ErrorResponse(w http.ResponseWriter, r *http.Request, code int, desc []Http // BadRequestResponse writes HTTP error 400 to w. func BadRequestResponse(w http.ResponseWriter, req *http.Request) { ErrorResponse(w, req, http.StatusBadRequest, []HttpErrorDesc{ - { "en", templateHttpErr400_EN }, - { "rs", templateHttpErr400_RS }, + {"en", templateHttpErr400_EN}, + {"rs", templateHttpErr400_RS}, }) } // InternalSeverErrorResponse writes HTTP error 500 to w. func InternalServerErrorResponse(w http.ResponseWriter, req *http.Request) { ErrorResponse(w, req, http.StatusInternalServerError, []HttpErrorDesc{ - { "en", templateHttpErr500_EN }, - { "rs", templateHttpErr500_RS }, + {"en", templateHttpErr500_EN}, + {"rs", templateHttpErr500_RS}, }) } // UnauthorizedError writes HTTP error 401 to w. func UnauthorizedResponse(w http.ResponseWriter, req *http.Request) { ErrorResponse(w, req, http.StatusUnauthorized, []HttpErrorDesc{ - { "en", templateHttpErr401_EN }, - { "rs", templateHttpErr401_RS }, + {"en", templateHttpErr401_EN}, + {"rs", templateHttpErr401_RS}, }) } -// TODO: Check for content type -// WrapHandler sets common headers, checks for token validity and performs access control checks. -// If authentication passes it calls the handlerFunc. -func WrapHandler(handlerFunc http.HandlerFunc, authorizedRoles []string) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - - w.Header().Set("Access-Control-Allow-Methods", - "POST, GET, PUT, DELETE, OPTIONS") - - w.Header().Set("Access-Control-Allow-Headers", - `Accept, Content-Type, Content-Length, - Accept-Encoding, X-CSRF-Token, Authorization`) - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - - if req.Method == "OPTIONS" { - return - } - - if authorizedRoles != nil { - token := req.Header.Get("Authorization") - claims, err := ParseAPIToken(token); - if err != nil || !roleAuthorized(authorizedRoles, claims) { - UnauthorizedResponse(w, req) - return - } - } - - err := req.ParseForm() - if err != nil { - BadRequestResponse(w, req) - return - } - - // execute HandlerFunc - handlerFunc(w, req) - } -} - // NotFoundHandler writes HTTP error 404 to w. func NotFoundHandler(w http.ResponseWriter, req *http.Request) { + SetDefaultHeaders(w) + if req.Method == "OPTIONS" { + return + } ErrorResponse(w, req, http.StatusNotFound, []HttpErrorDesc{ - { "en", "Not found." }, - { "rs", "Traženi resurs ne postoji." }, + {"en", "Not found."}, + {"rs", "Traženi resurs ne postoji."}, }) } + +func SetDefaultHeaders(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Origin", "*") + + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") + + w.Header().Set("Access-Control-Allow-Headers", `Accept, Content-Type, + Content-Length, Accept-Encoding, X-CSRF-Token, Authorization`) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") +} diff --git a/json_utility.go b/json_utility.go index 757bb7d..f178804 100644 --- a/json_utility.go +++ b/json_utility.go @@ -2,10 +2,10 @@ package webutility import ( //"fmt" - "net/http" "encoding/json" "errors" "io" + "net/http" //"io/ioutil" "sync" @@ -19,10 +19,10 @@ var payloads []payloadBuff type LangMap map[string]map[string]string type Field struct { - Parameter string `json:"param"` - Type string `json:"type"` - Visible bool `json:"visible"` - Editable bool `json:"editable"` + Parameter string `json:"param"` + Type string `json:"type"` + Visible bool `json:"visible"` + Editable bool `json:"editable"` } type CorrelationField struct { @@ -38,27 +38,27 @@ type Translation struct { type payloadBuff struct { Type string `json:"tableType"` - Method string `json:"method"` - Params map[string]string `json:"params"` - Lang []Translation `json:"lang"` - Fields []Field `json:"fields"` + Method string `json:"method"` + Params map[string]string `json:"params"` + Lang []Translation `json:"lang"` + Fields []Field `json:"fields"` Correlations []CorrelationField `json:"correlationFields"` IdField string `json:"idField"` // Data can only hold slices of any type. It can't be used for itteration - Data interface{} `json:"data"` + Data interface{} `json:"data"` } type Payload struct { - Method string `json:"method"` - Params map[string]string `json:"params"` - Lang []Translation `json:"lang"` - Fields []Field `json:"fields"` + Method string `json:"method"` + Params map[string]string `json:"params"` + Lang []Translation `json:"lang"` + Fields []Field `json:"fields"` Correlations []CorrelationField `json:"correlationFields"` IdField string `json:"idField"` // Data can only hold slices of any type. It can't be used for itteration - Data interface{} `json:"data"` + Data interface{} `json:"data"` } // NewPayload returs a payload sceleton for provided table. @@ -90,7 +90,7 @@ func translations(ptype string) []Translation { if pload.Type == ptype { for _, t := range pload.Lang { translations = append(translations, Translation{ - Language: t.Language, + Language: t.Language, FieldsLabels: t.FieldsLabels, }) } @@ -161,7 +161,7 @@ func InitTables(db *ora.Ses, project string) error { // Returns an error if it fails. func fetchJSON(db *ora.Ses, project string) ([]byte, error) { db.SetCfg(db.Cfg().SetClob(ora.S)) - stmt, err := db.Prep(`SELECT JSON_NCLOB FROM TABLES_CONFIG WHERE PROJEKAT` + EqualQuotes(project), ora.S) + stmt, err := db.Prep(`SELECT JSON_NCLOB FROM TABLES_CONFIG WHERE PROJEKAT`+EqualQuotes(project), ora.S) defer stmt.Close() if err != nil { return nil, err diff --git a/select_config.go b/select_config.go index 271bcf3..680acdb 100644 --- a/select_config.go +++ b/select_config.go @@ -17,8 +17,13 @@ func GetSelectConfig(db *ora.Ses, otype string) ([]SelectConfig, error) { resp := make([]SelectConfig, 0) var err error var stmt *ora.Stmt - query := `SELECT a.LIST_OBJECT_TYPE, a.OBJECT_TYPE, a.ID_FIELD, - a.LABEL_FIELD, a.TYPE, b.FIELD + query := `SELECT + a.LIST_OBJECT_TYPE, + a.OBJECT_TYPE, + a.ID_FIELD, + a.LABEL_FIELD, + a.TYPE, + b.FIELD FROM LIST_SELECT_CONFIG a, LIST_VALUE_FIELD b WHERE a.LIST_OBJECT_TYPE` + otype + ` AND b.LIST_TYPE = a.LIST_OBJECT_TYPE @@ -37,11 +42,11 @@ func GetSelectConfig(db *ora.Ses, otype string) ([]SelectConfig, error) { for rset.Next() { resp = append(resp, SelectConfig{ ListObjType: rset.Row[0].(string), - ObjType: rset.Row[1].(string), - IdField: rset.Row[2].(string), - LabelField: rset.Row[3].(string), - Type: rset.Row[4].(string), - ValueField: rset.Row[5].(string), + ObjType: rset.Row[1].(string), + IdField: rset.Row[2].(string), + LabelField: rset.Row[3].(string), + Type: rset.Row[4].(string), + ValueField: rset.Row[5].(string), }) } if rset.Err() != nil {