From 9af02771fdd4e01a622063dc1344fa0762eb6434 Mon Sep 17 00:00:00 2001 From: gezimbll Date: Fri, 14 Jul 2023 05:57:42 -0400 Subject: [PATCH] Added sentrypeer credentials in config && cache partition --- config/apiban.go | 33 ------ config/config_defaults.go | 4 +- config/libconfig_json.go | 5 +- config/sentrypeer.go | 58 ++++++++++ data/conf/samples/tutinternal/cgrates.json | 10 +- data/conf/samples/tutmongo/cgrates.json | 9 +- data/conf/samples/tutmysql/cgrates.json | 9 +- data/conf/samples/tutpostgres/cgrates.json | 9 +- engine/datamanager.go | 2 +- engine/filterhelpers.go | 122 +++++++++++++++------ engine/filters.go | 2 +- 11 files changed, 176 insertions(+), 87 deletions(-) create mode 100644 config/sentrypeer.go diff --git a/config/apiban.go b/config/apiban.go index fccd4ed43..f43eac3f9 100644 --- a/config/apiban.go +++ b/config/apiban.go @@ -55,36 +55,3 @@ func (ban APIBanCfg) Clone() (cln *APIBanCfg) { } return } - -type SentryPeerCfg struct { - Token string - Url string -} - -func (sp *SentryPeerCfg) loadFromJSONCfg(jsnCfg *SentryPeerJsonCfg) (err error) { - if jsnCfg == nil { - return - } - if jsnCfg.Url != nil { - sp.Url = *jsnCfg.Url - } - if jsnCfg.Token != nil { - sp.Token = *jsnCfg.Token - } - return -} - -func (sp *SentryPeerCfg) AsMapInterface() map[string]any { - return map[string]any{ - "URL": sp.Url, - "Token": sp.Token, - } -} - -func (sp *SentryPeerCfg) Clone() (cln *SentryPeerCfg) { - cln = &SentryPeerCfg{ - Url: sp.Url, - Token: sp.Token, - } - return -} diff --git a/config/config_defaults.go b/config/config_defaults.go index c9532d5b4..e539fc7a4 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -304,6 +304,7 @@ const CGRATES_CFG_JSON = ` "*uch": {"limit": -1, "ttl": "3h", "static_ttl": false, "remote":false, "replicate": false}, // User cache "*stir": {"limit": -1, "ttl": "3h", "static_ttl": false, "remote":false, "replicate": false}, // stirShaken cache keys "*apiban":{"limit": -1, "ttl": "2m", "static_ttl": false, "remote":false, "replicate": false}, + "*sentrypeer":{"limit": -1, "ttl": "86400s", "static_ttl": true, "remote":false, "replicate": false}, "*caps_events": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false}, // caps cached samples "*replication_hosts": {"limit": 0, "ttl": "", "static_ttl": false, "remote":false, "replicate": false}, // the replication hosts cache(used when replication_filtered is enbled) }, @@ -1268,7 +1269,8 @@ const CGRATES_CFG_JSON = ` }, "sentrypeer":{ "url":"", - "token":"", + "clientID":"", + "clientSecret":"", } }` diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 7c5912b38..9c1d760ff 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -877,8 +877,9 @@ type APIBanJsonCfg struct { } type SentryPeerJsonCfg struct { - Token *string - Url *string + ClientID *string + ClientSecret *string + Url *string } type CoreSJsonCfg struct { diff --git a/config/sentrypeer.go b/config/sentrypeer.go new file mode 100644 index 000000000..b1b73f85c --- /dev/null +++ b/config/sentrypeer.go @@ -0,0 +1,58 @@ +/* +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 config + +type SentryPeerCfg struct { + ClientID string + ClientSecret string + Url string +} + +func (sp *SentryPeerCfg) loadFromJSONCfg(jsnCfg *SentryPeerJsonCfg) (err error) { + if jsnCfg == nil { + return + } + if jsnCfg.Url != nil { + sp.Url = *jsnCfg.Url + } + if jsnCfg.ClientSecret != nil { + sp.ClientSecret = *jsnCfg.ClientSecret + } + if jsnCfg.ClientID != nil { + sp.ClientID = *jsnCfg.ClientID + } + return +} + +func (sp *SentryPeerCfg) AsMapInterface() map[string]any { + return map[string]any{ + "URL": sp.Url, + "ClientSecret": sp.ClientSecret, + "ClientID": sp.ClientID, + } +} + +func (sp *SentryPeerCfg) Clone() (cln *SentryPeerCfg) { + cln = &SentryPeerCfg{ + Url: sp.Url, + ClientSecret: sp.ClientSecret, + ClientID: sp.ClientID, + } + return +} diff --git a/data/conf/samples/tutinternal/cgrates.json b/data/conf/samples/tutinternal/cgrates.json index 422056db8..2382045c3 100644 --- a/data/conf/samples/tutinternal/cgrates.json +++ b/data/conf/samples/tutinternal/cgrates.json @@ -111,9 +111,11 @@ "apiers_conns": ["*internal"] } -// "sentrypeer":{ -// "url":"https://sentrypeer.com/api/", -// "token":"put token here", -// }, + //"sentrypeer":{ + //"url":"https://sentrypeer.com/api/", + //"clientID":"", + //"clientSecret":"", + //}, + } diff --git a/data/conf/samples/tutmongo/cgrates.json b/data/conf/samples/tutmongo/cgrates.json index 479fd592f..8ccbcc4dc 100644 --- a/data/conf/samples/tutmongo/cgrates.json +++ b/data/conf/samples/tutmongo/cgrates.json @@ -127,9 +127,10 @@ "apiers_conns": ["*internal"], }, - //"sentrypeer":{ - //"url":"https://sentrypeer.com/api/", - //"token":"" - //}, +// "sentrypeer":{ +// "url":"https://sentrypeer.com/api/", +// "clientID":"", +// "clientSecret":"", +// }, } diff --git a/data/conf/samples/tutmysql/cgrates.json b/data/conf/samples/tutmysql/cgrates.json index 5c1db20ec..7b20de59a 100644 --- a/data/conf/samples/tutmysql/cgrates.json +++ b/data/conf/samples/tutmysql/cgrates.json @@ -118,9 +118,10 @@ "apiers_conns": ["*internal"], }, -// "sentrypeer":{ -// "url":"https://sentrypeer.com/api/", -// "token":"put token here", -// }, + //"sentrypeer":{ + //"url":"https://sentrypeer.com/api/", + //"clientID":"", + //"clientSecret":"", + //}, } diff --git a/data/conf/samples/tutpostgres/cgrates.json b/data/conf/samples/tutpostgres/cgrates.json index 138d9e856..edf8600f6 100644 --- a/data/conf/samples/tutpostgres/cgrates.json +++ b/data/conf/samples/tutpostgres/cgrates.json @@ -103,9 +103,10 @@ "apiers_conns": ["*internal"], }, -// "sentrypeer":{ -// "url":"https://sentrypeer.com/api/", -// "token":"put token here", -// }, + //"sentrypeer":{ + //"url":"https://sentrypeer.com/api/", + //"clientID":"", + //"clientSecret":"", + //}, } diff --git a/engine/datamanager.go b/engine/datamanager.go index 4d61511c1..c170854ce 100644 --- a/engine/datamanager.go +++ b/engine/datamanager.go @@ -270,7 +270,7 @@ func (dm *DataManager) CacheDataFromDB(prfx string, ids []string, mustBeCached b case utils.MetaAPIBan: _, err = dm.GetAPIBan(utils.EmptyString, config.CgrConfig().APIBanCfg().Keys, false, false, true) case utils.MetaSentryPeer: - _, err = GetSentryPeer(utils.EmptyString, config.CgrConfig().SentryPeerCfg().Url, config.CgrConfig().SentryPeerCfg().Token, utils.EmptyString, false, true) + _, err = GetSentryPeer(utils.EmptyString, config.CgrConfig().SentryPeerCfg().Url, config.CgrConfig().SentryPeerCfg().ClientID, config.CgrConfig().SentryPeerCfg().ClientSecret, utils.EmptyString, false, true) } if err != nil { if err != utils.ErrNotFound && err != utils.ErrDSPProfileNotFound && err != utils.ErrDSPHostNotFound { diff --git a/engine/filterhelpers.go b/engine/filterhelpers.go index 431455723..b9e47b6bd 100644 --- a/engine/filterhelpers.go +++ b/engine/filterhelpers.go @@ -19,7 +19,10 @@ along with this program. If not, see package engine import ( + "bytes" + "encoding/json" "fmt" + "io" "net/http" "github.com/cgrates/cgrates/config" @@ -27,6 +30,10 @@ import ( "github.com/cgrates/cgrates/utils" ) +type TokenResponse struct { + AccessToken string `json:"access_token"` +} + // MatchingItemIDsForEvent returns the list of item IDs matching fieldName/fieldValue for an event // fieldIDs limits the fields which are checked against indexes // helper on top of dataDB.GetIndexes, adding utils.MetaAny to list of fields queried @@ -120,13 +127,51 @@ func WeightFromDynamics(dWs []*utils.DynamicWeight, } return 0.0, nil } - -func GetSentryPeer(val, url, token, path string, cacheRead, cacheWrite bool) (found bool, err error) { +func getToken(clientID, clientSecret string, cacheWrite bool, token *TokenResponse) (err error) { + var resp *http.Response + payload := map[string]string{ + "client_id": clientID, + "client_secret": clientSecret, + "audience": "https://sentrypeer.com/api", + "grant_type": "client_credentials", + } + jsonPayload, _ := json.Marshal(payload) + resp, err = getHTTP("POST", "https://authz.sentrypeer.com/oauth/token", bytes.NewBuffer(jsonPayload), map[string][]string{"Content-Type": {"application/json"}}) + if err != nil { + return err + } + defer resp.Body.Close() + err = json.NewDecoder(resp.Body).Decode(&token) + if err != nil { + return err + } + if cacheWrite { + if err = Cache.Set(utils.MetaSentryPeer, "*token", token.AccessToken, nil, true, utils.NonTransactional); err != nil { + return err + } + } + return +} +func GetSentryPeer(val, url, clientID, clientSecret, path string, cacheRead, cacheWrite bool) (found bool, err error) { valpath := utils.ConcatenatedKey(path, val) + var ( + token TokenResponse + resp *http.Response + cachedReq bool + ) if cacheRead { if x, ok := Cache.Get(utils.MetaSentryPeer, valpath); ok && x != nil { // Attempt to find in cache first return x.(bool), nil } + var cachedToken any + if cachedToken, cachedReq = Cache.Get(utils.MetaSentryPeer, "*token"); cachedReq && cachedToken != nil { + token.AccessToken = cachedToken.(string) + } + } + if !cachedReq { + if err = getToken(clientID, clientSecret, cacheWrite, &token); err != nil { + return + } } switch path { case "*ip": @@ -134,46 +179,57 @@ func GetSentryPeer(val, url, token, path string, cacheRead, cacheWrite bool) (fo case "*number": url += "phone-numbers/" } - var req *http.Request - if req, err = http.NewRequest("GET", url+val, nil); err != nil { + resp, err = getHTTP("GET", url+val, nil, map[string][]string{"Authorization": {fmt.Sprintf("Bearer %v", token.AccessToken)}}) + if err != nil { return } - if token != "" { - req.Header = http.Header{ - "Authorization": {fmt.Sprintf("Bearer %s", token)}, + defer resp.Body.Close() + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + Cache.Remove(utils.MetaSentryPeer, "*token", true, utils.NonTransactional) + utils.Logger.Warning("SentryPeer redirecting to new bearer token ") + if err = getToken(clientID, clientSecret, cacheWrite, &token); err != nil { + return + } + resp, err = getHTTP("GET", url+val, nil, map[string][]string{"Authorization": {fmt.Sprintf("Bearer %v", token.AccessToken)}}) + if err != nil { + return + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + return false, fmt.Errorf("still unauthorized after getting new token") } } - var resp *http.Response - resp, err = http.DefaultClient.Do(req) - if err != nil { - return false, err - } - defer resp.Body.Close() - if resp.StatusCode >= 200 && resp.StatusCode < 300 { + switch { + case resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices: + if cacheWrite { + if err = Cache.Set(utils.MetaSentryPeer, valpath, false, nil, true, utils.NonTransactional); err != nil { + return + } + } + return false, nil + case resp.StatusCode == http.StatusNotFound: if cacheWrite { if err = Cache.Set(utils.MetaSentryPeer, valpath, true, nil, true, utils.NonTransactional); err != nil { return false, err } } return true, nil - } else { - switch { - case resp.StatusCode == http.StatusNotFound: - if cacheWrite { - if err = Cache.Set(utils.MetaSentryPeer, valpath, false, nil, true, utils.NonTransactional); err != nil { - return false, err - } - } - return - case resp.StatusCode >= 400 && resp.StatusCode < 500: - err = fmt.Errorf("client error<%s>", resp.Status) - return - case resp.StatusCode >= 500: - err = fmt.Errorf("server error<%s>", resp.Status) - return - default: - err = fmt.Errorf("unexpected status code<%s>", resp.Status) - } + case resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError: + err = fmt.Errorf("client err <%v>", resp.Status) + case resp.StatusCode >= http.StatusInternalServerError: + err = fmt.Errorf("server error<%s>", resp.Status) + default: + err = fmt.Errorf("unexpected status code<%s>", resp.Status) } - return + utils.Logger.Warning(fmt.Sprintf("Sentrypeer filter got %v ", err.Error())) + return false, err +} + +func getHTTP(method, url string, payload io.Reader, headers map[string][]string) (resp *http.Response, err error) { + var req *http.Request + if req, err = http.NewRequest(method, url, payload); err != nil { + return + } + req.Header = headers + return http.DefaultClient.Do(req) } diff --git a/engine/filters.go b/engine/filters.go index 5c8890995..aba146666 100644 --- a/engine/filters.go +++ b/engine/filters.go @@ -659,7 +659,7 @@ func (fltr *FilterRule) passSentryPeer(dDP utils.DataProvider) (bool, error) { if fltr.Values[0] != "*number" && fltr.Values[0] != "*ip" { return false, fmt.Errorf("invalid value for sentrypeer filter: <%s>", fltr.Values[0]) } - return GetSentryPeer(strVal, config.CgrConfig().SentryPeerCfg().Url, config.CgrConfig().SentryPeerCfg().Token, fltr.Values[0], true, true) + return GetSentryPeer(strVal, config.CgrConfig().SentryPeerCfg().Url, config.CgrConfig().SentryPeerCfg().ClientID, config.CgrConfig().SentryPeerCfg().ClientSecret, fltr.Values[0], true, true) } func parseTime(rsr *config.RSRParser, dDp utils.DataProvider) (_ time.Time, err error) {