azkustodata/trusted_endpoints/trusted_endpoints.go (191 lines of code) (raw):

package trustedEndpoints import ( _ "embed" "encoding/json" "fmt" "math" "net/url" "strings" "github.com/Azure/azure-kusto-go/azkustodata/errors" "github.com/samber/lo" ) var ( Instance = createInstance() //go:embed well_known_kusto_endpoints.json jsonFile []byte ) type AllowedEndpoints struct { AllowedKustoSuffixes []string AllowedKustoHostnames []string } type WellKnownKustoEndpointsDataStruct struct { AllowedEndpointsByLogin map[string]AllowedEndpoints } func createInstance() *TrustedEndpoints { matchers := map[string]*FastSuffixMatcher{} wellKnownData := WellKnownKustoEndpointsDataStruct{} err := json.Unmarshal(jsonFile, &wellKnownData) if err != nil { panic(err.Error()) } for key, value := range wellKnownData.AllowedEndpointsByLogin { rules := []MatchRule{} for _, suf := range value.AllowedKustoSuffixes { rules = append(rules, MatchRule{suffix: suf, exact: false}) } for _, host := range value.AllowedKustoHostnames { rules = append(rules, MatchRule{suffix: host, exact: true}) } f, err := newFastSuffixMatcher(rules) if err != nil { panic(err.Error()) } matchers[key] = f } return &TrustedEndpoints{matchers: matchers} } // SetOverridePolicy Set a policy to override all other trusted rules func (trusted *TrustedEndpoints) SetOverridePolicy(matcher func(string) bool) { trusted.overrideMatcher = matcher } type TrustedEndpoints struct { matchers map[string]*FastSuffixMatcher additionalMatcher *FastSuffixMatcher overrideMatcher func(string) bool } type MatchRule struct { suffix string exact bool } type FastSuffixMatcher struct { suffixLength int rules map[string][]MatchRule } func tailLowerCase(str string, length int) string { if length <= 0 { return "" } if length >= len(str) { return strings.ToLower(str) } return strings.ToLower(str[len(str)-length:]) } func (matcher *FastSuffixMatcher) isMatch(candidate string) bool { if len(candidate) < matcher.suffixLength { return false } if lst, ok := matcher.rules[tailLowerCase(candidate, matcher.suffixLength)]; ok { for _, rule := range lst { if strings.HasSuffix(strings.ToLower(candidate), rule.suffix) { if len(candidate) == len(rule.suffix) || !rule.exact { return true } } } } return false } func newFastSuffixMatcher(rules []MatchRule) (*FastSuffixMatcher, error) { minSufLen := len(lo.MinBy(rules, func(a MatchRule, cur MatchRule) bool { return len(a.suffix) < len(cur.suffix) }).suffix) if minSufLen == 0 || minSufLen == math.MaxInt32 { return nil, errors.ES( errors.OpUnknown, errors.KClientArgs, "FastSuffixMatcher should have at list one rule with at least one character", ).SetNoRetry() } processedRules := map[string][]MatchRule{} for _, rule := range rules { suffix := tailLowerCase(rule.suffix, minSufLen) if lst, ok := processedRules[suffix]; !ok { processedRules[suffix] = []MatchRule{rule} } else { processedRules[suffix] = append(lst, rule) } } return &FastSuffixMatcher{ suffixLength: minSufLen, rules: processedRules, }, nil } func values[T comparable, R any](m map[T]R) []R { l := make([]R, 0, len(m)) for _, val := range m { l = append(l, val) } return l } func createFastSuffixMatcherFromExisting(rules []MatchRule, existing *FastSuffixMatcher) (*FastSuffixMatcher, error) { if existing == nil || len(existing.rules) == 0 { return newFastSuffixMatcher(rules) } if rules == nil || len(rules) == 0 { return existing, nil } for _, elem := range existing.rules { rules = append(rules, elem...) } return newFastSuffixMatcher(rules) } // AddTrustedHosts Add or set a list of trusted endpoints rules func (trusted *TrustedEndpoints) AddTrustedHosts(rules []MatchRule, replace bool) error { if rules == nil || len(rules) == 0 { if replace { trusted.additionalMatcher = nil } return nil } if replace { trusted.additionalMatcher = nil } matcher, err := createFastSuffixMatcherFromExisting(rules, trusted.additionalMatcher) trusted.additionalMatcher = matcher return err } // ValidateTrustedEndpoint Validates the endpoint uri trusted func (trusted *TrustedEndpoints) ValidateTrustedEndpoint(endpoint string, loginEndpoint string) error { u, err := url.Parse(endpoint) if err != nil { return err } host := u.Host if host == "" { host = endpoint } // Check that target hostname is trusted and can accept security token return trusted.validateHostnameIsTrusted(host, loginEndpoint) } func isLocalAddress(host string) bool { if host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "[::1]" { return true } if strings.HasPrefix(host, "127.") && len(host) <= 15 && len(host) >= 9 { for _, c := range host { if c != '.' && (c < '0' || c > '9') { return false } } return true } return false } func (trusted *TrustedEndpoints) validateHostnameIsTrusted(host string, loginEndpoint string) error { // The loopback is unconditionally allowed (since we trust ourselves) if isLocalAddress(host) { return nil } // Either check the override matcher OR the matcher: override := trusted.overrideMatcher if override != nil && override(host) { return nil } else { matcher, ok := trusted.matchers[strings.ToLower(loginEndpoint)] if ok && (*matcher).isMatch(host) { return nil } } matcher := trusted.additionalMatcher if matcher != nil && matcher.isMatch(host) { return nil } return errors.ES( errors.OpUnknown, errors.KClientArgs, fmt.Sprintf("Can't communicate with '%s' as this hostname is currently not trusted; please see https://aka.ms/kustotrustedendpoints.", host), ).SetNoRetry() }