diff --git a/config/apis.go b/config/apis.go index 19d07050b..aaa56b897 100644 --- a/config/apis.go +++ b/config/apis.go @@ -590,6 +590,12 @@ func storeDiffSection(ctx *context.Context, section string, db ConfigDB, v1, v2 return } return db.SetSection(ctx, section, diffAPIBanJsonCfg(jsn, v1.APIBanCfg(), v2.APIBanCfg())) + case SentryPeerJSON: + jsn := new(SentryPeerJsonCfg) + if err = db.GetSection(ctx, section, jsn); err != nil { + return + } + return db.SetSection(ctx, section, diffSentryPeerJsonCfg(jsn, v1.SentryPeerCfg(), v2.SentryPeerCfg())) case CoreSJSON: jsn := new(CoreSJsonCfg) if err = db.GetSection(ctx, section, jsn); err != nil { diff --git a/config/config.go b/config/config.go index 0da3ec78b..ed7f14c0c 100644 --- a/config/config.go +++ b/config/config.go @@ -245,10 +245,11 @@ func newCGRConfig(config []byte) (cfg *CGRConfig, err error) { ProfileIgnoreFilters: []*utils.DynamicBoolOpt{}, PosterAttempts: []*utils.DynamicIntOpt{}, }}, - sipAgentCfg: new(SIPAgentCfg), - configSCfg: new(ConfigSCfg), - apiBanCfg: new(APIBanCfg), - coreSCfg: new(CoreSCfg), + sipAgentCfg: new(SIPAgentCfg), + configSCfg: new(ConfigSCfg), + apiBanCfg: new(APIBanCfg), + sentryPeerCfg: new(SentryPeerCfg), + coreSCfg: new(CoreSCfg), accountSCfg: &AccountSCfg{Opts: &AccountsOpts{ ProfileIDs: []*utils.DynamicStringSliceOpt{}, Usage: []*utils.DynamicDecimalBigOpt{}, @@ -367,6 +368,7 @@ type CGRConfig struct { sipAgentCfg *SIPAgentCfg // SIPAgent config configSCfg *ConfigSCfg // ConfigS config apiBanCfg *APIBanCfg // APIBan config + sentryPeerCfg *SentryPeerCfg //SentryPeer config coreSCfg *CoreSCfg // CoreS config accountSCfg *AccountSCfg // AccountS config tpeSCfg *TpeSCfg // TpeS config @@ -761,6 +763,12 @@ func (cfg *CGRConfig) APIBanCfg() *APIBanCfg { return cfg.apiBanCfg } +func (cfg *CGRConfig) SentryPeerCfg() *SentryPeerCfg { + cfg.lks[SentryPeerJSON].Lock() + defer cfg.lks[SentryPeerJSON].Unlock() + return cfg.sentryPeerCfg +} + // CoreSCfg reads the CoreS configuration func (cfg *CGRConfig) CoreSCfg() *CoreSCfg { cfg.lks[CoreSJSON].Lock() @@ -1073,6 +1081,7 @@ func (cfg *CGRConfig) Clone() (cln *CGRConfig) { sipAgentCfg: cfg.sipAgentCfg.Clone(), configSCfg: cfg.configSCfg.Clone(), apiBanCfg: cfg.apiBanCfg.Clone(), + sentryPeerCfg: cfg.sentryPeerCfg.Clone(), coreSCfg: cfg.coreSCfg.Clone(), actionSCfg: cfg.actionSCfg.Clone(), accountSCfg: cfg.accountSCfg.Clone(), diff --git a/config/config_defaults.go b/config/config_defaults.go index 84e6f8cb8..895c84819 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -288,7 +288,8 @@ const CGRATES_CFG_JSON = ` "*rpc_connections": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false}, // RPC connections caching "*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}, + "*apiban":{"limit": -1, "ttl": "2m", "static_ttl": false, "remote":false, "replicate": false}, + "*sentrypeer":{"limit": -1, "ttl": "24h", "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) }, @@ -1800,6 +1801,16 @@ const CGRATES_CFG_JSON = ` "keys": [], }, +"sentrypeer":{ + "client_id":"", + "client_secret":"", + "token_url":"https://authz.sentrypeer.com/oauth/token", + "ips_url":"https://sentrypeer.com/api/ip-addresses", + "numbers_url":"https://sentrypeer.com/api/phone-numbers", + "audience":"https://sentrypeer.com/api", + "grant_type":"client_credentials", +}, + "actions": { // ActionS config "enabled": false, // starts attribute service: diff --git a/config/config_json.go b/config/config_json.go index 464a0b026..077e78239 100644 --- a/config/config_json.go +++ b/config/config_json.go @@ -69,6 +69,7 @@ const ( TemplatesJSON = "templates" ConfigSJSON = "configs" APIBanJSON = "apiban" + SentryPeerJSON = "sentrypeer" CoreSJSON = "cores" AccountSJSON = "accounts" ConfigDBJSON = "config_db" @@ -197,6 +198,7 @@ func newSections(cfg *CGRConfig) Sections { cfg.coreSCfg, cfg.configSCfg, cfg.apiBanCfg, + cfg.sentryPeerCfg, cfg.configDBCfg, cfg.sureTaxCfg, cfg.tpeSCfg, diff --git a/config/sentrypeercfg.go b/config/sentrypeercfg.go new file mode 100644 index 000000000..0d3a85b78 --- /dev/null +++ b/config/sentrypeercfg.go @@ -0,0 +1,118 @@ +package config + +import ( + "github.com/cgrates/birpc/context" + "github.com/cgrates/cgrates/utils" +) + +type SentryPeerCfg struct { + ClientID string + ClientSecret string + TokenUrl string + IpsUrl string + NumbersUrl string + Audience string + GrantType string +} + +func (sentrypeer *SentryPeerCfg) Load(ctx *context.Context, jsnCfg ConfigDB, _ *CGRConfig) (err error) { + jsnSentryPeerCfg := new(SentryPeerJsonCfg) + if err = jsnCfg.GetSection(ctx, SentryPeerJSON, jsnSentryPeerCfg); err != nil { + return + } + return sentrypeer.loadFromJSONCfg(jsnSentryPeerCfg) +} + +func (sentrypeer *SentryPeerCfg) loadFromJSONCfg(jsnCfg *SentryPeerJsonCfg) (err error) { + if jsnCfg == nil { + return + } + if jsnCfg.Client_id != nil { + sentrypeer.ClientID = *jsnCfg.Client_id + } + if jsnCfg.Client_secret != nil { + sentrypeer.ClientSecret = *jsnCfg.Client_secret + } + if jsnCfg.Token_url != nil { + sentrypeer.TokenUrl = *jsnCfg.Token_url + } + if jsnCfg.Ips_url != nil { + sentrypeer.IpsUrl = *jsnCfg.Ips_url + } + if jsnCfg.Numbers_url != nil { + sentrypeer.NumbersUrl = *jsnCfg.Numbers_url + } + if jsnCfg.Audience != nil { + sentrypeer.Audience = *jsnCfg.Audience + } + if jsnCfg.Grant_type != nil { + sentrypeer.GrantType = *jsnCfg.Grant_type + } + return +} +func (sentrypeer SentryPeerCfg) AsMapInterface(string) any { + return map[string]any{ + utils.ClientIDCfg: sentrypeer.ClientID, + utils.ClientSecretCfg: sentrypeer.ClientSecret, + utils.TokenUrlCfg: sentrypeer.TokenUrl, + utils.IpsUrlCfg: sentrypeer.IpsUrl, + utils.NumbersUrlCfg: sentrypeer.NumbersUrl, + utils.AudienceCfg: sentrypeer.Audience, + utils.GrantTypeCfg: sentrypeer.GrantType, + } +} + +func (SentryPeerCfg) SName() string { return SentryPeerJSON } +func (sentrypeer SentryPeerCfg) CloneSection() Section { return sentrypeer.Clone() } + +func (sentrypeer SentryPeerCfg) Clone() (cln *SentryPeerCfg) { + + return &SentryPeerCfg{ + ClientID: sentrypeer.ClientID, + ClientSecret: sentrypeer.ClientSecret, + TokenUrl: sentrypeer.TokenUrl, + IpsUrl: sentrypeer.IpsUrl, + NumbersUrl: sentrypeer.NumbersUrl, + Audience: sentrypeer.Audience, + GrantType: sentrypeer.GrantType, + } +} + +type SentryPeerJsonCfg struct { + Client_id *string + Client_secret *string + Token_url *string + Ips_url *string + Numbers_url *string + Audience *string + Grant_type *string +} + +func diffSentryPeerJsonCfg(d *SentryPeerJsonCfg, v1, v2 *SentryPeerCfg) *SentryPeerJsonCfg { + if d == nil { + d = new(SentryPeerJsonCfg) + } + if v1.ClientID != v2.ClientID { + d.Client_id = utils.StringPointer(v2.ClientID) + } + if v1.ClientSecret != v2.ClientSecret { + d.Client_secret = utils.StringPointer(v2.ClientSecret) + } + if v1.TokenUrl != v2.TokenUrl { + d.Token_url = utils.StringPointer(v2.TokenUrl) + } + if v1.IpsUrl != v2.IpsUrl { + d.Ips_url = utils.StringPointer(v2.IpsUrl) + } + if v1.NumbersUrl != v2.NumbersUrl { + d.Numbers_url = utils.StringPointer(v2.NumbersUrl) + } + if v1.Audience != v2.Audience { + d.Audience = utils.StringPointer(v2.Audience) + } + if v1.GrantType != v2.GrantType { + d.Grant_type = utils.StringPointer(v2.GrantType) + } + + return d +} diff --git a/engine/datamanager.go b/engine/datamanager.go index 670aa66dd..39f253356 100644 --- a/engine/datamanager.go +++ b/engine/datamanager.go @@ -120,7 +120,7 @@ func (dm *DataManager) CacheDataFromDB(ctx *context.Context, prfx string, ids [] return } // *apiban and *dispatchers are not stored in database - if prfx == utils.MetaAPIBan || prfx == utils.MetaDispatchers { // no need for ids in this case + if prfx == utils.MetaAPIBan || prfx == utils.MetaDispatchers || prfx == utils.MetaSentryPeer { // no need for ids in this case ids = []string{utils.EmptyString} } else if len(ids) != 0 && ids[0] == utils.MetaAny { if mustBeCached { diff --git a/engine/filterhelpers.go b/engine/filterhelpers.go index c2b7164b8..dfc770fc0 100644 --- a/engine/filterhelpers.go +++ b/engine/filterhelpers.go @@ -19,6 +19,13 @@ along with this program. If not, see package engine import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/guardian" @@ -137,3 +144,124 @@ func BlockerFromDynamics(ctx *context.Context, dBs []*utils.DynamicBlocker, } return false, nil } + +func GetSentryPeer(ctx *context.Context, val string, sentryPeerCfg *config.SentryPeerCfg, dataType string) (found bool, err error) { + itemId := utils.ConcatenatedKey(dataType, val) + var ( + isCached bool + apiUrl 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 dataType { + case utils.MetaIp: + apiUrl, err = url.JoinPath(sentryPeerCfg.IpsUrl, val) + case utils.MetaNumber: + apiUrl, err = url.JoinPath(sentryPeerCfg.NumbersUrl, val) + } + if err != nil { + return + } + if !isCached { + if token, err = sentrypeerGetToken(sentryPeerCfg.TokenUrl, sentryPeerCfg.ClientID, sentryPeerCfg.ClientSecret, + sentryPeerCfg.Audience, sentryPeerCfg.GrantType); err != nil { + utils.Logger.Err(fmt.Sprintf("sentrypeer token auth got err <%v> ", err.Error())) + return + } + if err = Cache.Set(ctx, utils.MetaSentryPeer, utils.MetaToken, + token, nil, true, utils.NonTransactional); err != nil { + return + } + } + + for i := 0; i < 2; i++ { + if found, err = sentrypeerHasData(itemId, token, apiUrl); err == nil { + if err = Cache.Set(ctx, utils.MetaSentryPeer, itemId, found, + nil, true, ""); err != nil { + return + } + break + } else if err != utils.ErrNotAuthorized { + utils.Logger.Err(err.Error()) + break + } + utils.Logger.Warning("Sentrypeer token expired !Getting new one.") + Cache.Remove(ctx, utils.MetaSentryPeer, utils.MetaToken, true, utils.EmptyString) + if token, err = sentrypeerGetToken(sentryPeerCfg.TokenUrl, sentryPeerCfg.ClientID, sentryPeerCfg.ClientSecret, + sentryPeerCfg.Audience, sentryPeerCfg.GrantType); err != nil { + return + } + if err = Cache.Set(ctx, utils.MetaSentryPeer, utils.MetaToken, token, + nil, true, utils.NonTransactional); err != nil { + return + } + } + return +} + +// Returns a new token from sentrypeer api +func sentrypeerGetToken(tokenUrl, clientID, clientSecret, audience, grantType string) (token string, err error) { + var resp *http.Response + payload := map[string]string{ + utils.ClientIDCfg: clientID, + utils.ClientSecretCfg: clientSecret, + utils.AudienceCfg: audience, + utils.GrantTypeCfg: grantType, + } + jsonPayload, _ := json.Marshal(payload) + resp, err = getHTTP(http.MethodPost, tokenUrl, bytes.NewBuffer(jsonPayload), map[string][]string{"Content-Type": {"application/json"}}) + if err != nil { + return + } + defer resp.Body.Close() + var m struct { + AccessToken string `json:"access_token"` + } + err = json.NewDecoder(resp.Body).Decode(&m) + if err != nil { + return + } + token = m.AccessToken + return +} + +// sentrypeerHasData return a boolean based on query response on finding ip/number +func sentrypeerHasData(itemId, token, url string) (found bool, err error) { + var resp *http.Response + resp, err = getHTTP(http.MethodGet, url, nil, map[string][]string{"Authorization": {fmt.Sprintf("Bearer %s", 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: + return true, nil + case resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError: + err = fmt.Errorf("sentrypeer api got client err <%v>", resp.Status) + case resp.StatusCode >= http.StatusInternalServerError: + err = fmt.Errorf("sentrypeer api got server err <%s>", resp.Status) + default: + err = fmt.Errorf("sentrypeer api got unexpected err <%s>", resp.Status) + } + 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 9833a3506..a6cb7bbdb 100644 --- a/engine/filters.go +++ b/engine/filters.go @@ -226,18 +226,18 @@ var supportedFiltersType utils.StringSet = utils.NewStringSet([]string{ utils.MetaCronExp, utils.MetaRSR, utils.MetaEmpty, utils.MetaExists, utils.MetaLessThan, utils.MetaLessOrEqual, utils.MetaGreaterThan, utils.MetaGreaterOrEqual, utils.MetaEqual, - utils.MetaIPNet, utils.MetaAPIBan, utils.MetaActivationInterval, + utils.MetaIPNet, utils.MetaAPIBan, utils.MetaSentryPeer, utils.MetaActivationInterval, utils.MetaRegex, utils.MetaNever}) var needsFieldName utils.StringSet = utils.NewStringSet([]string{ utils.MetaString, utils.MetaPrefix, utils.MetaSuffix, utils.MetaCronExp, utils.MetaRSR, utils.MetaLessThan, utils.MetaEmpty, utils.MetaExists, utils.MetaLessOrEqual, utils.MetaGreaterThan, - utils.MetaGreaterOrEqual, utils.MetaEqual, utils.MetaIPNet, utils.MetaAPIBan, + utils.MetaGreaterOrEqual, utils.MetaEqual, utils.MetaIPNet, utils.MetaAPIBan, utils.MetaSentryPeer, utils.MetaActivationInterval, utils.MetaRegex}) var needsValues utils.StringSet = utils.NewStringSet([]string{ utils.MetaString, utils.MetaPrefix, utils.MetaSuffix, utils.MetaCronExp, utils.MetaRSR, utils.MetaLessThan, utils.MetaLessOrEqual, utils.MetaGreaterThan, utils.MetaGreaterOrEqual, - utils.MetaEqual, utils.MetaIPNet, utils.MetaAPIBan, utils.MetaActivationInterval, + utils.MetaEqual, utils.MetaIPNet, utils.MetaAPIBan, utils.MetaSentryPeer, utils.MetaActivationInterval, utils.MetaRegex}) // NewFilterRule returns a new filter @@ -365,6 +365,8 @@ func (fltr *FilterRule) Pass(ctx *context.Context, dDP utils.DataProvider) (resu result, err = fltr.passIPNet(dDP) case utils.MetaAPIBan, utils.MetaNotAPIBan: result, err = fltr.passAPIBan(ctx, dDP) + case utils.MetaSentryPeer, utils.MetaNotSentryPeer: + result, err = fltr.passSentryPeer(ctx, dDP) case utils.MetaActivationInterval, utils.MetaNotActivationInterval: result, err = fltr.passActivationInterval(dDP) case utils.MetaRegex, utils.MetaNotRegex: @@ -627,6 +629,20 @@ func (fltr *FilterRule) passAPIBan(ctx *context.Context, dDP utils.DataProvider) return GetAPIBan(ctx, strVal, config.CgrConfig().APIBanCfg().Keys, fltr.Values[0] != utils.MetaAll, true, true) } +func (fltr *FilterRule) passSentryPeer(ctx *context.Context, dDP utils.DataProvider) (bool, error) { + strVal, err := fltr.rsrElement.ParseDataProvider(dDP) + if err != nil { + if err == utils.ErrNotFound { + return false, nil + } + return false, err + } + 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(ctx, strVal, config.CgrConfig().SentryPeerCfg(), fltr.Values[0]) +} + func parseTime(rsr *config.RSRParser, dDp utils.DataProvider) (_ time.Time, err error) { var str string if str, err = rsr.ParseDataProvider(dDp); err != nil { diff --git a/engine/libtest.go b/engine/libtest.go index a42414323..fe15f5dc4 100644 --- a/engine/libtest.go +++ b/engine/libtest.go @@ -417,6 +417,7 @@ func GetDefaultEmptyCacheStats() map[string]*ltcache.CacheStats { utils.CacheEventCharges: {}, utils.CacheReverseFilterIndexes: {}, utils.MetaAPIBan: {}, + utils.MetaSentryPeer: {}, utils.CacheCapsEvents: {}, utils.CacheActionProfiles: {}, utils.CacheActionProfilesFilterIndexes: {}, diff --git a/utils/consts.go b/utils/consts.go index 145644a0e..a5d074ed9 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1060,6 +1060,10 @@ const ( MetaEqual = "*eq" MetaIPNet = "*ipnet" MetaAPIBan = "*apiban" + MetaSentryPeer = "*sentrypeer" + MetaToken = "*token" + MetaIp = "*ip" + MetaNumber = "*number" MetaActivationInterval = "*ai" MetaRegex = "*regex" MetaNever = "*never" @@ -1076,6 +1080,7 @@ const ( MetaNotEqual = "*noteq" MetaNotIPNet = "*notipnet" MetaNotAPIBan = "*notapiban" + MetaNotSentryPeer = "*notsentrypeer" MetaNotActivationInterval = "*notai" MetaNotRegex = "*notregex" @@ -2212,6 +2217,16 @@ const ( KeysCfg = "keys" ) +const ( + ClientIDCfg = "client_id" + ClientSecretCfg = "client_secret" + TokenUrlCfg = "token_url" + IpsUrlCfg = "ips_url" + NumbersUrlCfg = "numbers_url" + AudienceCfg = "audience" + GrantTypeCfg = "grant_type" +) + // STIR/SHAKEN const ( STIRAlg = "ES256" diff --git a/utils/errors.go b/utils/errors.go index 1e01d2152..848a94335 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -65,6 +65,7 @@ var ( ErrNotConnected = errors.New("NOT_CONNECTED") DispatcherErrorPrefix = "DISPATCHER_ERROR" RateSErrPrfx = "RATES_ERROR" + ErrNotAuthorized = errors.New("NOT_AUTHORIZED") AccountSErrPrfx = "ACCOUNTS_ERROR" ErrLoggerChanged = errors.New("LOGGER_CHANGED") ErrUnsupportedFormat = errors.New("UNSUPPORTED_FORMAT")