From 0cfd025a0db501aa3f7e61a7f8111c488c9c870d Mon Sep 17 00:00:00 2001 From: Shane Neuerburg Date: Thu, 10 Nov 2016 16:35:08 -0700 Subject: [PATCH] Add basic authentication without dependencies This adds a rudimentary basic auth scheme without including dependencies. --- cmd/cgr-engine/cgr-engine.go | 5 +- config/config.go | 52 +++++++++--------- config/config_defaults.go | 5 +- config/config_json.go | 8 +-- config/libconfig_json.go | 9 ++-- data/conf/cgrates/cgrates.json | 8 +-- glide.lock | 19 ++----- glide.yaml | 2 - utils/basic_auth.go | 98 ++++++++++++++++++++++++++++++++++ utils/basic_auth_test.go | 47 ++++++++++++++++ utils/server.go | 9 ++-- 11 files changed, 192 insertions(+), 70 deletions(-) create mode 100644 utils/basic_auth.go create mode 100644 utils/basic_auth_test.go diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index 32253a4b3..298999d99 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -532,9 +532,8 @@ func startRpc(server *utils.Server, internalRaterChan, go server.ServeGOB(cfg.RPCGOBListen) go server.ServeHTTP( cfg.HTTPListen, - cfg.HTTPApiUseBasicAuth, - cfg.HTTPApiBasicAuthRealm, - cfg.HTTPApiHtpasswdFile, + cfg.HTTPUseBasicAuth, + cfg.HTTPAuthUsers, ) } diff --git a/config/config.go b/config/config.go index 3b255e9c4..7485a7395 100644 --- a/config/config.go +++ b/config/config.go @@ -200,25 +200,24 @@ type CGRConfig struct { StorDBCDRSIndexes []string DBDataEncoding string // The encoding used to store object data in strings: CacheConfig *CacheConfig - RPCJSONListen string // RPC JSON listening address - RPCGOBListen string // RPC GOB listening address - HTTPListen string // HTTP listening address - HTTPApiUseBasicAuth bool // Use basic auth for HTTP API - HTTPApiBasicAuthRealm string // Basic auth realm URL - HTTPApiHtpasswdFile string // Basic auth htpasswd file path - DefaultReqType string // Use this request type if not defined on top - DefaultCategory string // set default type of record - DefaultTenant string // set default tenant - DefaultTimezone string // default timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB> - Reconnects int // number of recconect attempts in case of connection lost <-1 for infinite | nb> - ConnectTimeout time.Duration // timeout for RPC connection attempts - ReplyTimeout time.Duration // timeout replies if not reaching back - ConnectAttempts int // number of initial connection attempts before giving up - ResponseCacheTTL time.Duration // the life span of a cached response - InternalTtl time.Duration // maximum duration to wait for internal connections before giving up - RoundingDecimals int // Number of decimals to round end prices at - HttpSkipTlsVerify bool // If enabled Http Client will accept any TLS certificate - TpExportPath string // Path towards export folder for offline Tariff Plans + RPCJSONListen string // RPC JSON listening address + RPCGOBListen string // RPC GOB listening address + HTTPListen string // HTTP listening address + HTTPUseBasicAuth bool // Use basic auth for HTTP API + HTTPAuthUsers map[string]string // Basic auth user:password map (base64 passwords) + DefaultReqType string // Use this request type if not defined on top + DefaultCategory string // set default type of record + DefaultTenant string // set default tenant + DefaultTimezone string // default timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB> + Reconnects int // number of recconect attempts in case of connection lost <-1 for infinite | nb> + ConnectTimeout time.Duration // timeout for RPC connection attempts + ReplyTimeout time.Duration // timeout replies if not reaching back + ConnectAttempts int // number of initial connection attempts before giving up + ResponseCacheTTL time.Duration // the life span of a cached response + InternalTtl time.Duration // maximum duration to wait for internal connections before giving up + RoundingDecimals int // Number of decimals to round end prices at + HttpSkipTlsVerify bool // If enabled Http Client will accept any TLS certificate + TpExportPath string // Path towards export folder for offline Tariff Plans HttpPosterAttempts int HttpFailedDir string // Directory path where we store failed http requests MaxCallDuration time.Duration // The maximum call duration (used by responder when querying DerivedCharging) // ToDo: export it in configuration file @@ -506,7 +505,7 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { return err } - jsnHttpApiCfg, err := jsnCfg.HttpApiJsonCfg() + jsnHttpCfg, err := jsnCfg.HttpJsonCfg() if err != nil { return err } @@ -787,15 +786,12 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { } } - if jsnHttpApiCfg != nil { - if jsnHttpApiCfg.Use_basic_auth != nil { - self.HTTPApiUseBasicAuth = *jsnHttpApiCfg.Use_basic_auth + if jsnHttpCfg != nil { + if jsnHttpCfg.Use_basic_auth != nil { + self.HTTPUseBasicAuth = *jsnHttpCfg.Use_basic_auth } - if jsnHttpApiCfg.Basic_auth_realm != nil { - self.HTTPApiBasicAuthRealm = *jsnHttpApiCfg.Basic_auth_realm - } - if jsnHttpApiCfg.Htpasswd_file != nil { - self.HTTPApiHtpasswdFile = *jsnHttpApiCfg.Htpasswd_file + if jsnHttpCfg.Auth_users != nil { + self.HTTPAuthUsers = *jsnHttpCfg.Auth_users } } diff --git a/config/config_defaults.go b/config/config_defaults.go index b65eeaa6b..280002de6 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -72,10 +72,9 @@ const CGRATES_CFG_JSON = ` }, -"http_api": { // HTTP API configuration +"http": { // HTTP API configuration "use_basic_auth": false, // use basic authentication - "basic_auth_realm": "", // basic auth realm URL - "htpasswd_file": "" // basic auth htpasswd file location + "auth_users": {} // basic authentication usernames and base64-encoded passwords (eg: { "username1": "cGFzc3dvcmQ=", "username2": "cGFzc3dvcmQy "}) }, diff --git a/config/config_json.go b/config/config_json.go index 6246bbf7e..4bded865a 100644 --- a/config/config_json.go +++ b/config/config_json.go @@ -29,7 +29,7 @@ const ( GENERAL_JSN = "general" CACHE_JSN = "cache" LISTEN_JSN = "listen" - HTTP_API_JSN = "http_api" + HTTP_JSN = "http" TPDB_JSN = "tariffplan_db" DATADB_JSN = "data_db" STORDB_JSN = "stor_db" @@ -119,12 +119,12 @@ func (self CgrJsonCfg) ListenJsonCfg() (*ListenJsonCfg, error) { return cfg, nil } -func (self CgrJsonCfg) HttpApiJsonCfg() (*HTTPApiJsonCfg, error) { - rawCfg, hasKey := self[HTTP_API_JSN] +func (self CgrJsonCfg) HttpJsonCfg() (*HTTPJsonCfg, error) { + rawCfg, hasKey := self[HTTP_JSN] if !hasKey { return nil, nil } - cfg := new(HTTPApiJsonCfg) + cfg := new(HTTPJsonCfg) if err := json.Unmarshal(*rawCfg, cfg); err != nil { return nil, err } diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 45b92efec..c2aee8e17 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -46,11 +46,10 @@ type ListenJsonCfg struct { Http *string } -// HTTP API config section -type HTTPApiJsonCfg struct { - Use_basic_auth *bool - Basic_auth_realm *string - Htpasswd_file *string +// HTTP config section +type HTTPJsonCfg struct { + Use_basic_auth *bool + Auth_users *map[string]string } // Database config diff --git a/data/conf/cgrates/cgrates.json b/data/conf/cgrates/cgrates.json index 6eddf6e3b..710f062d8 100644 --- a/data/conf/cgrates/cgrates.json +++ b/data/conf/cgrates/cgrates.json @@ -51,12 +51,12 @@ // }, -// "http_api" { // HTTP API configuration -// "use_basic_auth": false, // use basic authentication -// "basic_auth_realm": "", // basic auth realm URL -// "htpasswd_file": "", // basic auth htpasswd file location +// "http": { // HTTP API configuration +// "use_basic_auth": false, // use basic authentication +// "auth_users": {} // basic authentication usernames and base64-encoded passwords (eg: { "username1": "cGFzc3dvcmQ=", "username2": "cGFzc3dvcmQy "}) // }, + // "tariffplan_db": { // database used to store active tariff plan configuration // "db_type": "redis", // tariffplan_db type: // "db_host": "127.0.0.1", // tariffplan_db host address diff --git a/glide.lock b/glide.lock index 83f928653..db06470c9 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,6 @@ -hash: cfda8a78a96bf7b3b471463d5ddf3330d6ae2089d5ac7c9a31b0f312e6518595 -updated: 2016-11-10T08:26:28.162167756-07:00 +hash: da953ea34fabe4e21f4dde6344f3b7ab5a75e02122a1c17af5ea434058fc77fb +updated: 2016-09-06T20:33:01.649869367+02:00 imports: -- name: github.com/abbot/go-http-auth - version: efc9484eee77263a11f158ef4f30fcc30298a942 - name: github.com/bit4bit/gami version: 3a7f98e7efce7ed7f22c2169b666910b8abb15dc - name: github.com/cenk/hub @@ -28,17 +26,13 @@ imports: - internal/parser/findutil - internal/parser/intfns - internal/parser/pathexpr - - literals/boollit - - literals/numlit - - literals/strlit + - internal/xconst + - internal/xsort - tree - tree/xmltree - tree/xmltree/xmlbuilder - tree/xmltree/xmlele - tree/xmltree/xmlnode - - xconst - - xfn - - xsort - name: github.com/DisposaBoy/JsonConfigReader version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4 - name: github.com/fiorix/go-diameter @@ -82,11 +76,6 @@ imports: version: 5cd0f2b3b6cca8e3a0a4101821e41a73cb59bed6 subpackages: - codec -- name: golang.org/x/crypto - version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd - subpackages: - - bcrypt - - blowfish - name: golang.org/x/net version: 1358eff22f0dd0c54fc521042cc607f6ff4b531a subpackages: diff --git a/glide.yaml b/glide.yaml index 6a48d4ba8..0d927866f 100644 --- a/glide.yaml +++ b/glide.yaml @@ -39,5 +39,3 @@ import: - package: github.com/hashicorp/golang-lru - package: github.com/cgrates/aringo - package: github.com/bit4bit/gami -- package: github.com/abbot/go-http-auth - version: ~0.3.0 diff --git a/utils/basic_auth.go b/utils/basic_auth.go new file mode 100644 index 000000000..eeb5decdd --- /dev/null +++ b/utils/basic_auth.go @@ -0,0 +1,98 @@ +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ +package utils + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" +) + +// use provides a cleaner interface for chaining middleware for single routes. +// Middleware functions are simple HTTP handlers (w http.ResponseWriter, r *http.Request) +// +// r.HandleFunc("/login", use(loginHandler, rateLimit, csrf)) +// r.HandleFunc("/form", use(formHandler, csrf)) +// r.HandleFunc("/about", aboutHandler) +// +// From https://gist.github.com/elithrar/9146306 +// See https://gist.github.com/elithrar/7600878#comment-955958 for how to extend it to suit simple http.Handler's +func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc { + for _, m := range middleware { + h = m(h) + } + + return h +} + +type basicAuthMiddleware func(h http.HandlerFunc) http.HandlerFunc + +// basicAuth returns a middleware function to intercept the request and validate +func basicAuth(userList map[string]string) basicAuthMiddleware { + return func(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + + authHeader := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + if len(authHeader) != 2 { + Logger.Warning(" Missing authorization header value") + http.Error(w, "Not authorized", 401) + return + } + + authHeaderDecoded, err := base64.StdEncoding.DecodeString(authHeader[1]) + if err != nil { + Logger.Warning(" Unable to decode authorization header") + http.Error(w, err.Error(), 401) + return + } + + userPass := strings.SplitN(string(authHeaderDecoded), ":", 2) + if len(userPass) != 2 { + Logger.Warning(" Unauthorized API access. Missing or extra credential components") + http.Error(w, "Not authorized", 401) + return + } + + valid := verifyCredential(userPass[0], userPass[1], userList) + if !valid { + Logger.Warning(fmt.Sprintf(" Unauthorized API access by user '%s'", userPass[0])) + http.Error(w, "Not authorized", 401) + return + } + + h.ServeHTTP(w, r) + } + } +} + +// verifyCredential validates the incoming username and password against the authorized user list +func verifyCredential(username string, password string, userList map[string]string) bool { + hash, ok := userList[username] + if !ok { + return false + } + + storedPass, err := base64.StdEncoding.DecodeString(hash) + if err != nil { + return false + } + + return string(storedPass[:]) == password +} diff --git a/utils/basic_auth_test.go b/utils/basic_auth_test.go new file mode 100644 index 000000000..14e33a779 --- /dev/null +++ b/utils/basic_auth_test.go @@ -0,0 +1,47 @@ +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ +package utils + +import "testing" + +func TestVerifyCredential(t *testing.T) { + var hashedPasswords = map[string]string{ + "1234": "MTIzNA==", + "bar": "YmFy", + } + + var verifyCredentialTests = []struct { + username string + password string + userList map[string]string + result bool + }{ + {"test", "1234", map[string]string{"test": hashedPasswords["1234"]}, true}, + {"test", "0000", map[string]string{"test": hashedPasswords["1234"]}, false}, + {"foo", "bar", map[string]string{"test": "1234", "foo": hashedPasswords["bar"]}, true}, + {"foo", "1234", map[string]string{"test": "1234", "foo": hashedPasswords["bar"]}, false}, + {"none", "1234", map[string]string{"test": "1234", "foo": hashedPasswords["bar"]}, false}, + } + + for _, tt := range verifyCredentialTests { + r := verifyCredential(tt.username, tt.password, tt.userList) + if r != tt.result { + t.Errorf("verifyCredential(%s, %s, %v) => %t, want %t", tt.username, tt.password, tt.userList, r, tt.result) + } + } +} diff --git a/utils/server.go b/utils/server.go index 44f4b3631..14d510485 100644 --- a/utils/server.go +++ b/utils/server.go @@ -29,7 +29,6 @@ import ( "reflect" "time" - "github.com/abbot/go-http-auth" "github.com/cenk/rpc2" ) import _ "net/http/pprof" @@ -147,13 +146,11 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { io.Copy(w, res) } -func (s *Server) ServeHTTP(addr string, useBasicAuth bool, basicAuthRealm string, htpasswdFile string) { +func (s *Server) ServeHTTP(addr string, useBasicAuth bool, userList map[string]string) { if s.rpcEnabled { if useBasicAuth { - Logger.Info(fmt.Sprintf("Configuring CGRateS HTTP server to use basic auth (realm: %s, htpasswd: %s).", basicAuthRealm, htpasswdFile)) - secrets := auth.HtpasswdFileProvider(htpasswdFile) - authenticator := auth.NewBasicAuthenticator(basicAuthRealm, secrets) - http.HandleFunc("/jsonrpc", auth.JustCheck(authenticator, handleRequest)) + Logger.Info("Configuring CGRateS HTTP server to use basic auth") + http.HandleFunc("/jsonrpc", use(handleRequest, basicAuth(userList))) } else { http.HandleFunc("/jsonrpc", handleRequest) }