From 2e93354ad1dbf0275f7436d3506b09f7dacb46a4 Mon Sep 17 00:00:00 2001 From: gezimbll Date: Fri, 14 Jul 2023 10:56:26 -0400 Subject: [PATCH] Added new fields in sentrypeerCfg && implemented fallback loop on unauthorized requests --- config/config_defaults.go | 12 +- config/libconfig_json.go | 10 +- config/sentrypeer.go | 34 ++++- data/conf/samples/tutinternal/cgrates.json | 6 - data/conf/samples/tutmongo/cgrates.json | 12 +- data/conf/samples/tutmysql/cgrates.json | 5 - engine/datamanager.go | 2 +- engine/filterhelpers.go | 150 +++++++++++---------- engine/filters.go | 4 +- utils/consts.go | 3 + utils/errors.go | 1 + 11 files changed, 135 insertions(+), 104 deletions(-) diff --git a/config/config_defaults.go b/config/config_defaults.go index e539fc7a4..3f5dff186 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -1268,9 +1268,13 @@ const CGRATES_CFG_JSON = ` "keys": [], }, "sentrypeer":{ - "url":"", - "clientID":"", - "clientSecret":"", -} + "client_id":"", + "client_secret":"", + "token_url":"https://authz.sentrypeer.com/oauth/token", + "ip_url":"https://sentrypeer.com/api/ip-addresses", + "number_url":"https://sentrypeer.com/api/phone-numbers", + "audience":"https://sentrypeer.com/api", + "grant_type":"client_credentials", + }, }` diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 9c1d760ff..a4d4ef8be 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -877,9 +877,13 @@ type APIBanJsonCfg struct { } type SentryPeerJsonCfg struct { - ClientID *string - ClientSecret *string - Url *string + ClientID *string `json:"client_id"` + ClientSecret *string `json:"client_secret"` + TokenUrl *string `json:"token_url"` + IpUrl *string `json:"ip_url"` + NumberUrl *string `json:"number_url"` + Audience *string `json:"audience"` + GrantType *string `json:"grant_type"` } type CoreSJsonCfg struct { diff --git a/config/sentrypeer.go b/config/sentrypeer.go index b1b73f85c..80c9f0a2c 100644 --- a/config/sentrypeer.go +++ b/config/sentrypeer.go @@ -21,15 +21,25 @@ package config type SentryPeerCfg struct { ClientID string ClientSecret string - Url string + TokenUrl string + IpUrl string + NumberUrl string + Audience string + GrantType string } func (sp *SentryPeerCfg) loadFromJSONCfg(jsnCfg *SentryPeerJsonCfg) (err error) { if jsnCfg == nil { return } - if jsnCfg.Url != nil { - sp.Url = *jsnCfg.Url + if jsnCfg.TokenUrl != nil { + sp.TokenUrl = *jsnCfg.TokenUrl + } + if jsnCfg.IpUrl != nil { + sp.IpUrl = *jsnCfg.IpUrl + } + if jsnCfg.NumberUrl != nil { + sp.NumberUrl = *jsnCfg.NumberUrl } if jsnCfg.ClientSecret != nil { sp.ClientSecret = *jsnCfg.ClientSecret @@ -37,22 +47,36 @@ func (sp *SentryPeerCfg) loadFromJSONCfg(jsnCfg *SentryPeerJsonCfg) (err error) if jsnCfg.ClientID != nil { sp.ClientID = *jsnCfg.ClientID } + if jsnCfg.Audience != nil { + sp.Audience = *jsnCfg.Audience + } + if jsnCfg.GrantType != nil { + sp.GrantType = *jsnCfg.GrantType + } return } func (sp *SentryPeerCfg) AsMapInterface() map[string]any { return map[string]any{ - "URL": sp.Url, + "TokenURL": sp.TokenUrl, "ClientSecret": sp.ClientSecret, "ClientID": sp.ClientID, + "IpUrl": sp.IpUrl, + "NumberUrl": sp.NumberUrl, + "Audience": sp.Audience, + "GrantType": sp.GrantType, } } func (sp *SentryPeerCfg) Clone() (cln *SentryPeerCfg) { cln = &SentryPeerCfg{ - Url: sp.Url, + TokenUrl: sp.TokenUrl, ClientSecret: sp.ClientSecret, ClientID: sp.ClientID, + IpUrl: sp.IpUrl, + NumberUrl: sp.NumberUrl, + Audience: sp.Audience, + GrantType: sp.GrantType, } return } diff --git a/data/conf/samples/tutinternal/cgrates.json b/data/conf/samples/tutinternal/cgrates.json index 2382045c3..65c848c8c 100644 --- a/data/conf/samples/tutinternal/cgrates.json +++ b/data/conf/samples/tutinternal/cgrates.json @@ -111,11 +111,5 @@ "apiers_conns": ["*internal"] } - //"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 8ccbcc4dc..5c833b4d8 100644 --- a/data/conf/samples/tutmongo/cgrates.json +++ b/data/conf/samples/tutmongo/cgrates.json @@ -128,9 +128,13 @@ }, // "sentrypeer":{ -// "url":"https://sentrypeer.com/api/", -// "clientID":"", -// "clientSecret":"", -// }, +// "client_id":"", +// "client_secret":"", +// "token_url":"", +// "ip_url":"", +// "number_url":"", +// "audience":"", +// "grant_type":"", +// }, } diff --git a/data/conf/samples/tutmysql/cgrates.json b/data/conf/samples/tutmysql/cgrates.json index 7b20de59a..92229e4f3 100644 --- a/data/conf/samples/tutmysql/cgrates.json +++ b/data/conf/samples/tutmysql/cgrates.json @@ -118,10 +118,5 @@ "apiers_conns": ["*internal"], }, - //"sentrypeer":{ - //"url":"https://sentrypeer.com/api/", - //"clientID":"", - //"clientSecret":"", - //}, } diff --git a/engine/datamanager.go b/engine/datamanager.go index c170854ce..66634e99d 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().ClientID, config.CgrConfig().SentryPeerCfg().ClientSecret, utils.EmptyString, false, true) + _, err = GetSentryPeer(utils.EmptyString, config.CgrConfig().SentryPeerCfg(), utils.EmptyString) } if err != nil { if err != utils.ErrNotFound && err != utils.ErrDSPProfileNotFound && err != utils.ErrDSPHostNotFound { diff --git a/engine/filterhelpers.go b/engine/filterhelpers.go index b9e47b6bd..8b369373c 100644 --- a/engine/filterhelpers.go +++ b/engine/filterhelpers.go @@ -30,10 +30,6 @@ 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 @@ -127,101 +123,107 @@ func WeightFromDynamics(dWs []*utils.DynamicWeight, } return 0.0, nil } -func getToken(clientID, clientSecret string, cacheWrite bool, token *TokenResponse) (err error) { +func getToken(tokenUrl, clientID, clientSecret, audience, grantType string) (token string, 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", + "audience": audience, + "grant_type": grantType, } 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": - url += "ip-addresses/" - case "*number": - url += "phone-numbers/" - } - resp, err = getHTTP("GET", url+val, nil, map[string][]string{"Authorization": {fmt.Sprintf("Bearer %v", token.AccessToken)}}) + resp, err = getHTTP("POST", tokenUrl, bytes.NewBuffer(jsonPayload), map[string][]string{"Content-Type": {"application/json"}}) if err != nil { return } 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 { + var m struct { + AccessToken string `json:"access_token"` + } + err = json.NewDecoder(resp.Body).Decode(&m) + if err != nil { + return + } + token = m.AccessToken + return +} +func GetSentryPeer(val string, sentryPeerCfg *config.SentryPeerCfg, path string) (found bool, err error) { + itemId := utils.ConcatenatedKey(path, val) + var ( + isCached bool + url string + token string + ) + if x, ok := Cache.Get(utils.MetaSentryPeer, itemId); ok && x != nil { // Attempt to find in cache first + return x.(bool), nil + } + var cachedToken any + if cachedToken, isCached = Cache.Get(utils.MetaSentryPeer, + utils.MetaToken); isCached && cachedToken != nil { + token = cachedToken.(string) + } + switch path { + case utils.MetaIp: + url = sentryPeerCfg.IpUrl + "/" + val + case utils.MetaNumber: + url = sentryPeerCfg.NumberUrl + "/" + val + } + if !isCached { + if token, err = getToken(sentryPeerCfg.TokenUrl, sentryPeerCfg.ClientID, sentryPeerCfg.ClientSecret, + sentryPeerCfg.Audience, sentryPeerCfg.GrantType); err != nil { return } - resp, err = getHTTP("GET", url+val, nil, map[string][]string{"Authorization": {fmt.Sprintf("Bearer %v", token.AccessToken)}}) - if err != nil { + if err = Cache.Set(utils.MetaSentryPeer, utils.MetaToken, + token, nil, true, utils.NonTransactional); 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") - } } - 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 { + + for i := 0; i < 2; i++ { + if found, err = hasData(itemId, token, url); err == nil { + if err = Cache.Set(utils.MetaSentryPeer, itemId, found, + nil, true, utils.NonTransactional); err != nil { return } + break + } else if err != utils.ErrNotAuthorized { + break } + Cache.Remove(utils.MetaSentryPeer, utils.MetaToken, true, utils.EmptyString) + if token, err = getToken(sentryPeerCfg.TokenUrl, sentryPeerCfg.ClientID, sentryPeerCfg.ClientSecret, + sentryPeerCfg.Audience, sentryPeerCfg.GrantType); err != nil { + return + } + if err = Cache.Set(utils.MetaSentryPeer, utils.MetaToken, token, + nil, true, utils.NonTransactional); err != nil { + return + } + } + return +} + +func hasData(itemId, token, url string) (found bool, err error) { + var resp *http.Response + resp, err = getHTTP("GET", url, nil, map[string][]string{"Authorization": {fmt.Sprintf("Bearer %v", token)}}) + if err != nil { + return + } + defer resp.Body.Close() + switch { + case resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden: + return false, utils.ErrNotAuthorized + case resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices: 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 case resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError: - err = fmt.Errorf("client err <%v>", resp.Status) + err = fmt.Errorf("sentrypeer api got client err <%v>", resp.Status) case resp.StatusCode >= http.StatusInternalServerError: - err = fmt.Errorf("server error<%s>", resp.Status) + err = fmt.Errorf("sentrypeer api got server error<%s>", resp.Status) default: - err = fmt.Errorf("unexpected status code<%s>", resp.Status) + err = fmt.Errorf("sentrypeer api got unexpected status code<%s>", resp.Status) } - utils.Logger.Warning(fmt.Sprintf("Sentrypeer filter got %v ", err.Error())) + utils.Logger.Warning(err.Error()) return false, err } diff --git a/engine/filters.go b/engine/filters.go index aba146666..72978cf85 100644 --- a/engine/filters.go +++ b/engine/filters.go @@ -656,10 +656,10 @@ func (fltr *FilterRule) passSentryPeer(dDP utils.DataProvider) (bool, error) { } return false, err } - if fltr.Values[0] != "*number" && fltr.Values[0] != "*ip" { + if fltr.Values[0] != utils.MetaNumber && fltr.Values[0] != utils.MetaIp { return false, fmt.Errorf("invalid value for sentrypeer filter: <%s>", fltr.Values[0]) } - return GetSentryPeer(strVal, config.CgrConfig().SentryPeerCfg().Url, config.CgrConfig().SentryPeerCfg().ClientID, config.CgrConfig().SentryPeerCfg().ClientSecret, fltr.Values[0], true, true) + return GetSentryPeer(strVal, config.CgrConfig().SentryPeerCfg(), fltr.Values[0]) } func parseTime(rsr *config.RSRParser, dDp utils.DataProvider) (_ time.Time, err error) { diff --git a/utils/consts.go b/utils/consts.go index 2b0a9ed75..c762029be 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1159,6 +1159,9 @@ const ( MetaIPNet = "*ipnet" MetaAPIBan = "*apiban" MetaSentryPeer = "*sentrypeer" + MetaToken = "*token" + MetaIp = "*ip" + MetaNumber = "*number" MetaActivationInterval = "*ai" MetaRegex = "*regex" diff --git a/utils/errors.go b/utils/errors.go index b333ec1e1..05279db67 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -67,6 +67,7 @@ var ( RalsErrorPrfx = "RALS_ERROR" DispatcherErrorPrefix = "DISPATCHER_ERROR" RateSErrPrfx = "RATES_ERROR" + ErrNotAuthorized = errors.New("NOT_AUTHORIZED") ErrUnsupportedFormat = errors.New("UNSUPPORTED_FORMAT") ErrNoDatabaseConn = errors.New("NO_DATABASE_CONNECTION") ErrMaxIncrementsExceeded = errors.New("MAX_INCREMENTS_EXCEEDED")