From 205fb643edbc649bb474fbc9bccd7ca528831560 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Mon, 8 Sep 2025 14:28:23 -0600 Subject: [PATCH 01/14] First implementation of sync policies --- go.mod | 3 + pkg/rbac/main/main.go | 18 + pkg/rbac/ranger/client.go | 208 ++++++++++ pkg/rbac/ranger/mocks/Client.go | 117 ++++++ pkg/rbac/ranger/policy.go | 278 ++++++++++++++ pkg/rbac/ranger/ranger.go | 93 +++++ .../ranger/tests/ranger_policy_check_test.go | 356 ++++++++++++++++++ pkg/rbac/ranger/user_sync.go | 80 ++++ pkg/rbac/rbac.go | 10 + pkg/sql/parser/sql.go | 19 +- 10 files changed, 1180 insertions(+), 2 deletions(-) create mode 100644 pkg/rbac/main/main.go create mode 100644 pkg/rbac/ranger/client.go create mode 100644 pkg/rbac/ranger/mocks/Client.go create mode 100644 pkg/rbac/ranger/policy.go create mode 100644 pkg/rbac/ranger/ranger.go create mode 100644 pkg/rbac/ranger/tests/ranger_policy_check_test.go create mode 100644 pkg/rbac/ranger/user_sync.go create mode 100644 pkg/rbac/rbac.go diff --git a/go.mod b/go.mod index 6fa4cb9..4e89351 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/linkedin/goavro v2.1.0+incompatible github.com/shopspring/decimal v1.4.0 github.com/snowflakedb/gosnowflake v1.15.0 + github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.33.4 k8s.io/apimachinery v0.33.4 @@ -95,6 +96,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect @@ -102,6 +104,7 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/pkg/rbac/main/main.go b/pkg/rbac/main/main.go new file mode 100644 index 0000000..f70e0d7 --- /dev/null +++ b/pkg/rbac/main/main.go @@ -0,0 +1,18 @@ +package main + +import "github.com/patterninc/heimdall/pkg/rbac/ranger" + +func main() { + r := &ranger.ApacheRanger{ + URL: "http://localhost:6080", + Username: "admin", + Password: "", + ServiceName: "TrinoRanger", + SyncIntervalInMinutes: 5, + } + err := r.SyncState() + if err != nil { + panic(err) + } + +} diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go new file mode 100644 index 0000000..e7b3477 --- /dev/null +++ b/pkg/rbac/ranger/client.go @@ -0,0 +1,208 @@ +package ranger + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + "time" +) + +const ( + getUsersEndpoint = `/service/xusers/users` + getServicePoliciesEndpoint = `/service/public/v2/api/service/%s/policy` + getGroupsEndpoint = `/service/xusers/groups` +) + +type User struct { + ID int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + EmailAddress string `json:"emailAddress,omitempty"` + UserRoleList []string `json:"userRoleList,omitempty"` + Password string `json:"password,omitempty"` + SyncSource string `json:"syncSource,omitempty"` + GroupIdList []int64 `json:"groupIdList,omitempty"` +} + +type Group struct { + ID int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + SyncSource string `json:"syncSource,omitempty"` +} + +type getResponse struct { + PageSize int `json:"pageSize"` + StartIndex int `json:"startIndex"` + ResultSize int `json:"resultSize"` + VXUsers []*User `json:"vXUsers,omitempty"` + VXGroups []*Group `json:"vXGroups,omitempty"` +} + +//go:generate go run github.com/vektra/mockery/v2@v2.53.4 --name=Client --output=./mocks --outpkg=mocks + +type Client interface { + GetUsers() (map[string]*User, error) + GetGroups() (map[string]*Group, error) + GetPolicies(serviceName string) ([]*Policy, error) +} + +type client struct { + URL string `yaml:"url" json:"url" omitempty"` + Username string `yaml:"username" json:"username" omitempty"` + Password string `yaml:"password" json:"password" omitempty"` + client *http.Client +} + +func (c *client) GetUsers() (map[string]*User, error) { + + responses, err := c.executeBatchRequest(http.MethodGet, getUsersEndpoint) + if err != nil { + return nil, err + } + + usersMap := make(map[string]*User) + + // Process all response batches into the map + for _, resp := range responses { + // Use type assertion to get the correct response type + for _, user := range resp.VXUsers { + usersMap[user.Name] = user + } + + } + + log.Printf("Number of Ranger Users pulled: %d\n", len(usersMap)) + return usersMap, nil + +} + +func (c *client) GetGroups() (map[string]*Group, error) { + + responses, err := c.executeBatchRequest(http.MethodGet, getGroupsEndpoint) + if err != nil { + return nil, err + } + + groupsMap := make(map[string]*Group) + + for _, resp := range responses { + for _, group := range resp.VXGroups { + groupsMap[group.Name] = group + } + + } + + log.Printf("Number of Ranger Groups pulled: %d\n", len(groupsMap)) + return groupsMap, nil + +} + +func (c *client) GetPolicies(serviceName string) ([]*Policy, error) { + var policies []*Policy + err := c.executeRequest(http.MethodGet, fmt.Sprintf(getServicePoliciesEndpoint, serviceName), &policies, nil) + return policies, err +} + +func (c *client) createRequest(method, endpoint string, reqBody interface{}) (*http.Request, error) { + + // Ensure client exists + if c.client == nil { + c.client = &http.Client{} + } + + var jsonBody []byte + var err error + + // Marshal body if POST request + if reqBody != nil { + jsonBody, err = json.Marshal(reqBody) + if err != nil { + return nil, err + } + } + + // Create Request + req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.URL, endpoint), bytes.NewBuffer(jsonBody)) + if err != nil { + return nil, err + } + + // Add auth headers + req.SetBasicAuth(c.Username, c.Password) + req.Header.Set("Content-Type", "application/json") + + return req, nil + +} + +func (r *client) executeRequest(method string, endpoint string, v interface{}, reqBody interface{}) error { + + req, err := r.createRequest(method, endpoint, reqBody) + if err != nil { + return err + } + + resp, err := r.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + bodyBytes, _ := io.ReadAll(resp.Body) + bodyString := string(bodyBytes) + + if strings.Contains(bodyString, "INVALID_INPUT_DATA") { + return nil + } + + return fmt.Errorf("request to %s failed with status %s\n%s", req.URL.String(), resp.Status, bodyString) + } + + if v != nil { + return json.NewDecoder(resp.Body).Decode(v) + } + + return nil + +} + +// executeBatchRequest performs paginated API requests and returns all aggregated results +func (r *client) executeBatchRequest(method string, endpoint string) ([]getResponse, error) { + + results := make([]getResponse, 500) + pageSize := 200 + startIndex := 0 + + for { + + batchEndpoint := fmt.Sprintf("%s?pageSize=%d&startIndex=%d", endpoint, pageSize, startIndex) + + // Marshall into generic get + getResponse := &getResponse{} + if err := r.executeRequest(method, batchEndpoint, getResponse, nil); err != nil { + return nil, err + } + + // Add this batch's response to our results + results = append(results, *getResponse) + + fmt.Printf("%v - Pulled batch with %d items...\n", time.Now().Format("2006-01-02 15:04:05"), getResponse.ResultSize) + + if getResponse.ResultSize < int(pageSize) { + break + } + + startIndex += pageSize + + } + + return results, nil + +} diff --git a/pkg/rbac/ranger/mocks/Client.go b/pkg/rbac/ranger/mocks/Client.go new file mode 100644 index 0000000..9f5d423 --- /dev/null +++ b/pkg/rbac/ranger/mocks/Client.go @@ -0,0 +1,117 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package mocks + +import ( + ranger "github.com/patterninc/heimdall/pkg/rbac/ranger" + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// GetGroups provides a mock function with no fields +func (_m *Client) GetGroups() (map[string]*ranger.Group, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetGroups") + } + + var r0 map[string]*ranger.Group + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]*ranger.Group, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]*ranger.Group); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*ranger.Group) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPolicies provides a mock function with given fields: serviceName +func (_m *Client) GetPolicies(serviceName string) ([]*ranger.Policy, error) { + ret := _m.Called(serviceName) + + if len(ret) == 0 { + panic("no return value specified for GetPolicies") + } + + var r0 []*ranger.Policy + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]*ranger.Policy, error)); ok { + return rf(serviceName) + } + if rf, ok := ret.Get(0).(func(string) []*ranger.Policy); ok { + r0 = rf(serviceName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*ranger.Policy) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(serviceName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsers provides a mock function with no fields +func (_m *Client) GetUsers() (map[string]*ranger.User, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetUsers") + } + + var r0 map[string]*ranger.User + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]*ranger.User, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]*ranger.User); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*ranger.User) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/rbac/ranger/policy.go b/pkg/rbac/ranger/policy.go new file mode 100644 index 0000000..1ea213c --- /dev/null +++ b/pkg/rbac/ranger/policy.go @@ -0,0 +1,278 @@ +package ranger + +import ( + "regexp" + "strings" + + "github.com/patterninc/heimdall/pkg/sql/parser" +) + +const ( + allActionAccessType = "all" +) + +var ( + actionByName = map[string]parser.Action{ + "select": parser.SELECT, + "insert": parser.INSERT, + "update": parser.UPDATE, + "delete": parser.DELETE, + "create": parser.CREATE, + "drop": parser.DROP, + "use": parser.USE, + "alter": parser.ALTER, + "grant": parser.GRANT, + "revoke": parser.REVOKE, + "show": parser.SHOW, + "impersonate": parser.IMPERSONATE, + "execute": parser.EXECUTE, + "read_system_information": parser.READ_SYSTEM_INFORMATION, + "write_system_information": parser.WRITE_SYSTEM_INFORMATION, + } + + allActions = []parser.Action{ + parser.SELECT, + parser.INSERT, + parser.UPDATE, + parser.DELETE, + parser.CREATE, + parser.DROP, + parser.USE, + parser.ALTER, + parser.GRANT, + parser.REVOKE, + parser.SHOW, + parser.IMPERSONATE, + parser.EXECUTE, + parser.READ_SYSTEM_INFORMATION, + parser.WRITE_SYSTEM_INFORMATION, + } +) + +type Policy struct { + ID int `json:"id"` + GUID string `json:"guid"` + IsEnabled bool `json:"isEnabled"` + Version int `json:"version"` + Service string `json:"service"` + Name string `json:"name"` + PolicyType int `json:"policyType"` + PolicyPriority int `json:"policyPriority"` + Description string `json:"description"` + IsAuditEnabled bool `json:"isAuditEnabled"` + Resources Resource `json:"resources"` + AdditionalResources []Resource `json:"additionalResources"` + PolicyItems []PolicyItem `json:"policyItems"` + DenyPolicyItems []PolicyItem `json:"denyPolicyItems"` + AllowExceptions []PolicyItem `json:"allowExceptions"` + DenyExceptions []PolicyItem `json:"denyExceptions"` + ServiceType string `json:"serviceType"` + + supportedTables *regexp.Regexp // todo init that regexp +} + +type ResourceField struct { + Values []string `json:"values"` + IsExcludes bool `json:"isExcludes"` +} + +type Resource struct { + Schema ResourceField `json:"schema,omitempty"` + Catalog ResourceField `json:"catalog,omitempty"` + Table ResourceField `json:"table,omitempty"` + Column ResourceField `json:"column,omitempty"` +} + +type Access struct { + Type string `json:"type"` +} + +type PolicyItem struct { + Accesses []Access `json:"accesses"` + Users []string `json:"users,omitempty"` + Groups []string `json:"groups,omitempty"` + Actions []parser.Action +} + +type ControlledActions struct { + allowedActionsByUser map[string][]parser.Action + deniedActionsByUser map[string][]parser.Action +} + +func (p *Policy) init() error { + resourceRegexpParts := []string{} + for _, resource := range append([]Resource{p.Resources}, p.AdditionalResources...) { + resourceRegexpParts = append(resourceRegexpParts, resource.getMatchRegexp()) + } + p.supportedTables = regexp.MustCompile("^(" + strings.Join(resourceRegexpParts, "|") + ")$") + return nil +} + +func (p *Policy) controlAnAccess(access parser.Access) bool { + switch a := access.(type) { + case *parser.TableAccess: + return p.supportedTables.Match([]byte(a.QualifiedName())) + } + return false +} + +func (p *Policy) getControlledActions(usersByGroup map[string][]string) ControlledActions { + return ControlledActions{ + allowedActionsByUser: p.getAllAllowPolicyByUser(usersByGroup), + deniedActionsByUser: p.getAllDenyPoliciesByUser(usersByGroup), + } +} + +func (p *Policy) getAllAllowPolicyByUser(usersByGroup map[string][]string) map[string][]parser.Action { + allowPoliciesItem := policyItemsToActionsByUser(p.PolicyItems, usersByGroup) + excludeAllowPolicyItems := policyItemsToActionsByUser(p.AllowExceptions, usersByGroup) + + for user, actions := range excludeAllowPolicyItems { + if _, ok := allowPoliciesItem[user]; !ok { + continue + } + for action := range actions { + delete(allowPoliciesItem[user], action) + } + if len(allowPoliciesItem[user]) == 0 { + delete(allowPoliciesItem, user) + } + } + + result := map[string][]parser.Action{} + for user, actionsMap := range allowPoliciesItem { + actions := make([]parser.Action, 0, len(actionsMap)) + for action := range actionsMap { + actions = append(actions, action) + } + result[user] = actions + } + return result +} + +func (p *Policy) getAllDenyPoliciesByUser(usersByGroup map[string][]string) map[string][]parser.Action { + denyPoliciesItem := policyItemsToActionsByUser(p.DenyPolicyItems, usersByGroup) + excludeDenyPolicyItems := policyItemsToActionsByUser(p.DenyExceptions, usersByGroup) + + for user, actions := range excludeDenyPolicyItems { + if _, ok := denyPoliciesItem[user]; !ok { + continue + } + for action := range actions { + delete(denyPoliciesItem[user], action) + } + if len(denyPoliciesItem[user]) == 0 { + delete(denyPoliciesItem, user) + } + } + + result := map[string][]parser.Action{} + for user, actionsMap := range denyPoliciesItem { + actions := make([]parser.Action, 0, len(actionsMap)) + for action := range actionsMap { + actions = append(actions, action) + } + result[user] = actions + } + return result +} + +func (r *Resource) getMatchRegexp() string { + catalogPart := ".*" + if len(r.Catalog.Values) > 0 { + catalogRegexp := patternsToRegex(r.Catalog.Values) + if r.Catalog.IsExcludes { + catalogPart = "(?!" + catalogRegexp + ").*" + } else { + catalogPart = "(" + catalogRegexp + ")" + } + } + + schemaPart := ".*" + if len(r.Schema.Values) > 0 { + schemaRegexp := patternsToRegex(r.Schema.Values) + if r.Schema.IsExcludes { + schemaPart = "(?!" + schemaRegexp + ").*" + } else { + schemaPart = "(" + schemaRegexp + ")" + } + } + + tablePart := ".*" + if len(r.Table.Values) > 0 { + tableRegexp := patternsToRegex(r.Table.Values) + if r.Table.IsExcludes { + tablePart = "(?!" + tableRegexp + ").*" + } else { + tablePart = "(" + tableRegexp + ")" + } + } + + return catalogPart + `\.` + schemaPart + `\.` + tablePart +} + +func globToRegex(pattern string) string { + escaped := regexp.QuoteMeta(pattern) + escaped = strings.ReplaceAll(escaped, "\\*", ".*") + escaped = strings.ReplaceAll(escaped, "\\?", ".") + return escaped +} + +func patternsToRegex(patterns []string) string { + var regexes []string + for _, pat := range patterns { + regexes = append(regexes, globToRegex(pat)) + } + return strings.Join(regexes, "|") +} +func (p *PolicyItem) getPermissions() []parser.Action { + if p.Actions != nil { + return p.Actions + } + p.Actions = make([]parser.Action, 0) + for _, access := range p.Accesses { + accessType := strings.ToLower(access.Type) + + if accessType == allActionAccessType { + return allActions + } + if action, ok := actionByName[accessType]; ok { + p.Actions = append(p.Actions, action) + continue + } + } + return p.Actions + +} + +func policyItemsToActionsByUser(items []PolicyItem, usersByGroup map[string][]string) map[string]map[parser.Action]struct{} { + permissions := make(map[string]map[parser.Action]struct{}) + + for _, item := range items { + for _, user := range item.Users { + user = strings.ToLower(user) + if _, ok := permissions[user]; !ok { + permissions[user] = make(map[parser.Action]struct{}) + } + for _, action := range item.getPermissions() { + permissions[user][action] = struct{}{} + } + } + for _, group := range item.Groups { + users, ok := usersByGroup[group] + if !ok { + continue + } + for _, user := range users { + if _, ok := permissions[user]; !ok { + permissions[user] = make(map[parser.Action]struct{}) + } + for _, action := range item.getPermissions() { + permissions[user][action] = struct{}{} + } + } + } + } + + return permissions +} diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go new file mode 100644 index 0000000..5a60220 --- /dev/null +++ b/pkg/rbac/ranger/ranger.go @@ -0,0 +1,93 @@ +package ranger + +import ( + "context" + "log" + "strings" + "time" + + "github.com/patterninc/heimdall/pkg/sql/parser" +) + +type ApacheRanger struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` + Client Client + SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` + AccessReceiver parser.AccessReceiver + permitionsByUser map[string]*UserPermitions +} + +type PermitionStatus int + +const ( + PermitionStatusAllow PermitionStatus = iota + PermitionStatusDeny + PermitionStatusUnknown +) + +type UserPermitions struct { + AllowPolicys map[parser.Action][]*Policy + DenyPolicys map[parser.Action][]*Policy +} + +func (ar *ApacheRanger) Init(ctx context.Context) error { + // first time lets sync state explicitly + if err := ar.SyncState(); err != nil { + return err + } + ar.startSyncPolicies(ctx) + return nil +} + +func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { + user = strings.ToLower(user) + if _, ok := ar.permitionsByUser[user]; !ok { + log.Println("User not found in ranger policies", "user", user) + return false, nil + } + accessList, err := ar.AccessReceiver.ParseAccess(query) + if err != nil { + return false, err + } + + permitions := ar.permitionsByUser[user] + for _, access := range accessList { + for _, permition := range permitions.DenyPolicys[access.Action()] { + if permition.controlAnAccess(access) { + log.Println("Access denied by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) + return false, nil + } + } + for _, permition := range permitions.AllowPolicys[access.Action()] { + if permition.controlAnAccess(access) { + log.Println("Access allowed by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) + return true, nil + } + } + } + return false, nil +} + +func (ar *ApacheRanger) startSyncPolicies(ctx context.Context) { + go func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + ticker := time.NewTicker(time.Duration(ar.SyncIntervalInMinutes) * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Println("Stopping Apache Ranger sync goroutine") + return + case <-ticker.C: + log.Println("Syncing policies from Apache Ranger for service:", ar.ServiceName) + if err := ar.SyncState(); err != nil { + log.Println("Error syncing users and groups from Apache Ranger", "error", err) + } + } + } + }() +} diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go new file mode 100644 index 0000000..b126ee5 --- /dev/null +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -0,0 +1,356 @@ +package tests + +import ( + "testing" + + "github.com/patterninc/heimdall/pkg/rbac/ranger" + "github.com/patterninc/heimdall/pkg/rbac/ranger/mocks" + "github.com/patterninc/heimdall/pkg/sql/parser/trino" +) + +const ( + serviceName = "test_service" +) + +func TestRangerPolicyCheck(t *testing.T) { + tests := []struct { + name string + query string + username string + expectedResult bool + users map[string]*ranger.User + groups map[string]*ranger.Group + policies []*ranger.Policy + }{ + { + name: "User with direct allow policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "alice", + expectedResult: true, + users: map[string]*ranger.User{ + "alice": {ID: 1, Name: "alice", GroupIdList: []int64{1}}, + }, + groups: map[string]*ranger.Group{ + "group1": {ID: 1, Name: "group1"}, + }, + policies: []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{"alice"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + { + name: "User with group allow policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "bob", + expectedResult: true, + users: map[string]*ranger.User{ + "bob": {ID: 2, Name: "bob", GroupIdList: []int64{1}}, + }, + groups: map[string]*ranger.Group{ + "group1": {ID: 1, Name: "group1"}, + }, + policies: []*ranger.Policy{ + { + ID: 2, + GUID: "policy-2", + IsEnabled: true, + Name: "Allow select for group1", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group1"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + { + name: "User with deny policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "charlie", + expectedResult: false, + users: map[string]*ranger.User{ + "charlie": {ID: 3, Name: "charlie", GroupIdList: []int64{2}}, + }, + groups: map[string]*ranger.Group{ + "group2": {ID: 2, Name: "group2"}, + }, + policies: []*ranger.Policy{ + { + ID: 3, + GUID: "policy-3", + IsEnabled: true, + Name: "Deny select for charlie", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Users: []string{"charlie"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + { + name: "User without any policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "dave", + expectedResult: false, + users: map[string]*ranger.User{ + "dave": {ID: 4, Name: "dave", GroupIdList: []int64{}}, + }, + groups: map[string]*ranger.Group{}, + policies: []*ranger.Policy{}, + }, + { + name: "User with conflicting allow and deny policies (deny should take precedence)", + query: "SELECT * FROM default_catalog.public.table1", + username: "eve", + expectedResult: false, + users: map[string]*ranger.User{ + "eve": {ID: 5, Name: "eve", GroupIdList: []int64{3}}, + }, + groups: map[string]*ranger.Group{ + "group3": {ID: 3, Name: "group3"}, + }, + policies: []*ranger.Policy{ + { + ID: 4, + GUID: "policy-4", + IsEnabled: true, + Name: "Allow select for eve", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{"eve"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + { + ID: 5, + GUID: "policy-5", + IsEnabled: true, + Name: "Deny select for group3", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group3"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + { + name: "User has different type of an access", + query: "SHOW TABLES FROM default_catalog.public", + username: "frank", + expectedResult: false, + users: map[string]*ranger.User{ + "frank": {ID: 6, Name: "frank", GroupIdList: []int64{4}}, + }, + groups: map[string]*ranger.Group{ + "group4": {ID: 4, Name: "group4"}, + }, + policies: []*ranger.Policy{ + { + ID: 6, + GUID: "policy-6", + IsEnabled: true, + Name: "Allow show for group4", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group4"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + { + name: "User allowed via regexp in table name", + query: "SELECT * FROM default_catalog.public.table_xyz", + username: "grace", + expectedResult: true, + users: map[string]*ranger.User{ + "grace": {ID: 7, Name: "grace", GroupIdList: []int64{5}}, + }, + groups: map[string]*ranger.Group{ + "group5": {ID: 5, Name: "group5"}, + }, + policies: []*ranger.Policy{ + { + ID: 7, + GUID: "policy-7", + IsEnabled: true, + Name: "Allow select for group5 on tables matching regex", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"*"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group5"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + rbac := &ranger.ApacheRanger{ + AccessReceiver: trino.NewTrinoAccessReceiver("default_catalog"), + Client: getMockRangerClient(tt.users, tt.groups, tt.policies), + ServiceName: serviceName, + } + rbac.SyncState() + actualResult, err := rbac.HasAccess(tt.username, tt.query) + if err != nil { + t.Errorf("error checking access for %q: %v", tt.name, err) + return + } + if actualResult != tt.expectedResult { + t.Errorf("unexpected result for %q: got %v, want %v", tt.name, actualResult, tt.expectedResult) + } + }) + } + +} + +func getMockRangerClient(users map[string]*ranger.User, groups map[string]*ranger.Group, policies []*ranger.Policy) *mocks.Client { + m := new(mocks.Client) + m.On("GetUsers").Return(users, nil) + m.On("GetGroups").Return(groups, nil) + m.On("GetPolicies", serviceName).Return(policies, nil) + return m +} diff --git a/pkg/rbac/ranger/user_sync.go b/pkg/rbac/ranger/user_sync.go new file mode 100644 index 0000000..7d5fd2e --- /dev/null +++ b/pkg/rbac/ranger/user_sync.go @@ -0,0 +1,80 @@ +package ranger + +import ( + "log" + + "github.com/patterninc/heimdall/pkg/sql/parser" +) + +func (r *ApacheRanger) SyncState() error { + users, err := r.Client.GetUsers() + if err != nil { + return err + } + groups, err := r.Client.GetGroups() + if err != nil { + return err + } + policies, err := r.Client.GetPolicies(r.ServiceName) + if err != nil { + return err + } + println("Users:", len(users), "Groups:", len(groups), "Policies:", len(policies)) + + groupByID := map[int64]*Group{} + usersByGroup := map[string][]string{} + for _, group := range groups { + groupByID[group.ID] = group + } + + for _, user := range users { + for _, gid := range user.GroupIdList { + if group, ok := groupByID[gid]; ok { + usersByGroup[group.Name] = append(usersByGroup[group.Name], user.Name) + } + } + } + + newPermitionsByUser := map[string]*UserPermitions{} + for _, policy := range policies { + if !policy.IsEnabled { + continue + } + if len(policy.Resources.Catalog.Values) == 0 || len(policy.Resources.Schema.Values) == 0 || len(policy.Resources.Table.Values) == 0 { + // Skip policies that do not have catalog, schema, and table defined + continue + } + + if err := policy.init(); err != nil { + log.Println("Error initializing policy:", err) + return err + } + controlledActions := policy.getControlledActions(usersByGroup) + for userName, actions := range controlledActions.allowedActionsByUser { + if _, ok := newPermitionsByUser[userName]; !ok { + newPermitionsByUser[userName] = &UserPermitions{ + AllowPolicys: map[parser.Action][]*Policy{}, + DenyPolicys: map[parser.Action][]*Policy{}, + } + } + for _, action := range actions { + newPermitionsByUser[userName].AllowPolicys[action] = append(newPermitionsByUser[userName].AllowPolicys[action], policy) + } + } + for userName, actions := range controlledActions.deniedActionsByUser { + if _, ok := newPermitionsByUser[userName]; !ok { + newPermitionsByUser[userName] = &UserPermitions{ + AllowPolicys: map[parser.Action][]*Policy{}, + DenyPolicys: map[parser.Action][]*Policy{}, + } + } + for _, action := range actions { + newPermitionsByUser[userName].DenyPolicys[action] = append(newPermitionsByUser[userName].DenyPolicys[action], policy) + } + } + } + + r.permitionsByUser = newPermitionsByUser + log.Println("Syncing users and groups from Apache Ranger for service:", r.ServiceName) + return nil +} diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go new file mode 100644 index 0000000..a037ee7 --- /dev/null +++ b/pkg/rbac/rbac.go @@ -0,0 +1,10 @@ +package rbac + +import ( + "context" +) + +type RBAC interface { + Init(ctx context.Context) error + HasAccess(user string, query string) (bool, error) +} diff --git a/pkg/sql/parser/sql.go b/pkg/sql/parser/sql.go index 7be8c87..c8c3b2b 100644 --- a/pkg/sql/parser/sql.go +++ b/pkg/sql/parser/sql.go @@ -1,5 +1,7 @@ package parser +import "fmt" + type AccessKind int const ( @@ -13,16 +15,25 @@ type Action int const ( SELECT Action = iota INSERT - UPDATE - DELETE CREATE DROP + DELETE + USE ALTER + GRANT + REVOKE + SHOW + IMPERSONATE + EXECUTE + UPDATE + READ_SYSTEM_INFORMATION + WRITE_SYSTEM_INFORMATION ) type Access interface { Kind() AccessKind Action() Action + QualifiedName() string } type TableAccess struct { @@ -35,6 +46,10 @@ type TableAccess struct { func (t *TableAccess) Kind() AccessKind { return TableAccessKind } func (t *TableAccess) Action() Action { return t.Act } +func (t *TableAccess) QualifiedName() string { + return fmt.Sprintf("%s.%s.%s", t.Catalog, t.Schema, t.Table) +} + type AccessReceiver interface { ParseAccess(sql string) ([]Access, error) } From 691561656c3aab7680d9950c7c05e85b64917e11 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Mon, 8 Sep 2025 16:43:45 -0600 Subject: [PATCH 02/14] Add more tests --- pkg/rbac/main/main.go | 22 +- .../ranger/tests/ranger_policy_check_test.go | 224 ++++++++++++++++++ 2 files changed, 235 insertions(+), 11 deletions(-) diff --git a/pkg/rbac/main/main.go b/pkg/rbac/main/main.go index f70e0d7..1cb7b14 100644 --- a/pkg/rbac/main/main.go +++ b/pkg/rbac/main/main.go @@ -3,16 +3,16 @@ package main import "github.com/patterninc/heimdall/pkg/rbac/ranger" func main() { - r := &ranger.ApacheRanger{ - URL: "http://localhost:6080", - Username: "admin", - Password: "", - ServiceName: "TrinoRanger", - SyncIntervalInMinutes: 5, - } - err := r.SyncState() - if err != nil { - panic(err) - } + // r := &ranger.ApacheRanger{ + // URL: "http://localhost:6080", + // Username: "admin", + // Password: "", + // ServiceName: "TrinoRanger", + // SyncIntervalInMinutes: 5, + // } + // err := r.SyncState() + // if err != nil { + // panic(err) + // } } diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index b126ee5..3b14e63 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -143,6 +143,14 @@ func TestRangerPolicyCheck(t *testing.T) { IsExcludes: false, }, }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{"charlie"}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, DenyPolicyItems: []ranger.PolicyItem{ { Users: []string{"charlie"}, @@ -323,6 +331,222 @@ func TestRangerPolicyCheck(t *testing.T) { }, }, }, + { + name: "User denied via regexp in table name", + query: "SELECT * FROM default_catalog.public.table_abc", + username: "heidi", + expectedResult: false, + users: map[string]*ranger.User{ + "heidi": {ID: 8, Name: "heidi", GroupIdList: []int64{6}}, + }, + groups: map[string]*ranger.Group{ + "group6": {ID: 6, Name: "group6"}, + }, + policies: []*ranger.Policy{ + { + ID: 8, + GUID: "policy-8", + IsEnabled: true, + Name: "Deny select for group6 on tables matching regex", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table_*"}, + IsExcludes: false, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group6"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group6"}, + Accesses: []ranger.Access{ + {Type: "*"}, + }, + }, + }, + }, + }, + }, + { + name: "User is exclude from allow policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "ivan", + expectedResult: false, + users: map[string]*ranger.User{ + "ivan": {ID: 9, Name: "ivan", GroupIdList: []int64{7}}, + }, + groups: map[string]*ranger.Group{ + "group7": {ID: 7, Name: "group7"}, + }, + policies: []*ranger.Policy{ + { + ID: 9, + GUID: "policy-9", + IsEnabled: true, + Name: "Allow select for group7 excluding user ivan", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group7"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + AllowExceptions: []ranger.PolicyItem{ + { + Users: []string{"ivan"}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + }, + }, + }, + { + name: "User is denied via group policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "judy", + expectedResult: false, + users: map[string]*ranger.User{ + "judy": {ID: 10, Name: "judy", GroupIdList: []int64{8}}, + }, + groups: map[string]*ranger.Group{ + "group8": {ID: 8, Name: "group8"}, + }, + policies: []*ranger.Policy{ + { + ID: 10, + GUID: "policy-10", + IsEnabled: true, + Name: "Deny select for group8 excluding user judy", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{"judy"}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group8"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, + { + name: "User is exclude from deny policy", + query: "SELECT * FROM default_catalog.public.table1", + username: "judy", + expectedResult: true, + users: map[string]*ranger.User{ + "judy": {ID: 10, Name: "judy", GroupIdList: []int64{8}}, + }, + groups: map[string]*ranger.Group{ + "group8": {ID: 8, Name: "group8"}, + }, + policies: []*ranger.Policy{ + { + ID: 10, + GUID: "policy-10", + IsEnabled: true, + Name: "Deny select for group8 excluding user judy", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{"judy"}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group8"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + DenyExceptions: []ranger.PolicyItem{ + { + Users: []string{"judy"}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { From fac36c1503e40ed5bfcab34a41baa5b336f727bd Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Wed, 10 Sep 2025 10:33:20 -0600 Subject: [PATCH 03/14] Add more tests --- .../ranger/tests/ranger_policy_check_test.go | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index 3b14e63..7a44ea1 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -547,6 +547,50 @@ func TestRangerPolicyCheck(t *testing.T) { }, }, }, + { + name: "User is allowed when resource has excludes", + query: "SELECT * FROM default_catalog.public.table1", + username: "kate", + expectedResult: true, + users: map[string]*ranger.User{ + "kate": {ID: 11, Name: "kate", GroupIdList: []int64{9}}, + }, + groups: map[string]*ranger.Group{ + "group9": {ID: 9, Name: "group9"}, + }, + policies: []*ranger.Policy{ + { + ID: 11, + GUID: "policy-11", + IsEnabled: true, + Name: "Allow select for group9 excluding schema 'internal'", + PolicyType: 0, + PolicyPriority: 1, + Resources: ranger.Resource{ + Catalog: ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: ranger.ResourceField{ + Values: []string{"internal"}, + IsExcludes: true, + }, + Table: ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{"group9"}, + Accesses: []ranger.Access{ + {Type: "select"}, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { From 2bddc28785f7272cc622f266d24731f897a13b11 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Wed, 10 Sep 2025 15:15:15 -0600 Subject: [PATCH 04/14] Add more tests --- pkg/rbac/main/main.go | 30 +- pkg/rbac/ranger/client.go | 8 + pkg/rbac/ranger/policy.go | 100 +-- pkg/rbac/ranger/ranger.go | 11 +- .../tests/ranger_deny_user_policy_test.go | 165 +++++ .../ranger/tests/ranger_policy_check_test.go | 608 ++++++++++++++++-- pkg/rbac/ranger/user_sync.go | 11 +- 7 files changed, 809 insertions(+), 124 deletions(-) create mode 100644 pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go diff --git a/pkg/rbac/main/main.go b/pkg/rbac/main/main.go index 1cb7b14..f559185 100644 --- a/pkg/rbac/main/main.go +++ b/pkg/rbac/main/main.go @@ -1,18 +1,24 @@ package main -import "github.com/patterninc/heimdall/pkg/rbac/ranger" +import ( + "github.com/patterninc/heimdall/pkg/rbac/ranger" + "github.com/patterninc/heimdall/pkg/sql/parser/trino" +) func main() { - // r := &ranger.ApacheRanger{ - // URL: "http://localhost:6080", - // Username: "admin", - // Password: "", - // ServiceName: "TrinoRanger", - // SyncIntervalInMinutes: 5, - // } - // err := r.SyncState() - // if err != nil { - // panic(err) - // } + r := &ranger.ApacheRanger{ + Name: "LocalRanger", + Client: ranger.NewClient("http://localhost:6080", "admin", ""), + ServiceName: "TrinoRanger", + AccessReceiver: trino.NewTrinoAccessReceiver("glue_catalog"), + SyncIntervalInMinutes: 5, + } + err := r.SyncState() + if err != nil { + panic(err) + } + + println(r.HasAccess("ihladush", "SELECT * FROM test.test_table")) + println(r.HasAccess("ivan.hladush@pattern.com", "SELECT * FROM test.test_table")) } diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go index e7b3477..4b9ddf5 100644 --- a/pkg/rbac/ranger/client.go +++ b/pkg/rbac/ranger/client.go @@ -59,6 +59,14 @@ type client struct { client *http.Client } +func NewClient(url, username, password string) Client { + return &client{ + URL: url, + Username: username, + Password: password, + client: &http.Client{}, + } +} func (c *client) GetUsers() (map[string]*User, error) { responses, err := c.executeBatchRequest(http.MethodGet, getUsersEndpoint) diff --git a/pkg/rbac/ranger/policy.go b/pkg/rbac/ranger/policy.go index 1ea213c..931d1f2 100644 --- a/pkg/rbac/ranger/policy.go +++ b/pkg/rbac/ranger/policy.go @@ -60,27 +60,26 @@ type Policy struct { PolicyPriority int `json:"policyPriority"` Description string `json:"description"` IsAuditEnabled bool `json:"isAuditEnabled"` - Resources Resource `json:"resources"` - AdditionalResources []Resource `json:"additionalResources"` + Resources *Resource `json:"resources"` + AdditionalResources []*Resource `json:"additionalResources"` PolicyItems []PolicyItem `json:"policyItems"` DenyPolicyItems []PolicyItem `json:"denyPolicyItems"` AllowExceptions []PolicyItem `json:"allowExceptions"` DenyExceptions []PolicyItem `json:"denyExceptions"` ServiceType string `json:"serviceType"` - - supportedTables *regexp.Regexp // todo init that regexp } type ResourceField struct { Values []string `json:"values"` IsExcludes bool `json:"isExcludes"` + regexp *regexp.Regexp } type Resource struct { - Schema ResourceField `json:"schema,omitempty"` - Catalog ResourceField `json:"catalog,omitempty"` - Table ResourceField `json:"table,omitempty"` - Column ResourceField `json:"column,omitempty"` + Schema *ResourceField `json:"schema,omitempty"` + Catalog *ResourceField `json:"catalog,omitempty"` + Table *ResourceField `json:"table,omitempty"` + Column *ResourceField `json:"column,omitempty"` } type Access struct { @@ -100,18 +99,59 @@ type ControlledActions struct { } func (p *Policy) init() error { - resourceRegexpParts := []string{} - for _, resource := range append([]Resource{p.Resources}, p.AdditionalResources...) { - resourceRegexpParts = append(resourceRegexpParts, resource.getMatchRegexp()) + for _, v := range append([]*Resource{p.Resources}, p.AdditionalResources...) { + if len(v.Catalog.Values) != 0 { + v.Catalog.regexp = regexp.MustCompile("^(" + patternsToRegex(v.Catalog.Values) + ")$") + } + if len(v.Schema.Values) != 0 { + v.Schema.regexp = regexp.MustCompile("^(" + patternsToRegex(v.Schema.Values) + ")$") + } + if len(v.Table.Values) != 0 { + v.Table.regexp = regexp.MustCompile("^(" + patternsToRegex(v.Table.Values) + ")$") + } } - p.supportedTables = regexp.MustCompile("^(" + strings.Join(resourceRegexpParts, "|") + ")$") return nil } func (p *Policy) controlAnAccess(access parser.Access) bool { switch a := access.(type) { case *parser.TableAccess: - return p.supportedTables.Match([]byte(a.QualifiedName())) + return p.controlTableAccess(a) + } + return false +} + +func (p *Policy) controlTableAccess(a *parser.TableAccess) bool { + for _, v := range append([]*Resource{p.Resources}, p.AdditionalResources...) { + if len(v.Catalog.Values) != 0 { + matchCatalog := v.Catalog.regexp.MatchString(a.Catalog) + if matchCatalog && v.Catalog.IsExcludes { + continue + } + if !matchCatalog && !v.Catalog.IsExcludes { + continue + } + } + if len(v.Schema.Values) != 0 { + matchSchema := v.Schema.regexp.MatchString(a.Schema) + if matchSchema && v.Schema.IsExcludes { + continue + } + if !matchSchema && !v.Schema.IsExcludes { + continue + } + } + if len(v.Table.Values) != 0 { + matchTable := v.Table.regexp.MatchString(a.Table) + if matchTable && v.Table.IsExcludes { + continue + } + if !matchTable && !v.Table.IsExcludes { + continue + } + } + + return true } return false } @@ -177,40 +217,6 @@ func (p *Policy) getAllDenyPoliciesByUser(usersByGroup map[string][]string) map[ return result } -func (r *Resource) getMatchRegexp() string { - catalogPart := ".*" - if len(r.Catalog.Values) > 0 { - catalogRegexp := patternsToRegex(r.Catalog.Values) - if r.Catalog.IsExcludes { - catalogPart = "(?!" + catalogRegexp + ").*" - } else { - catalogPart = "(" + catalogRegexp + ")" - } - } - - schemaPart := ".*" - if len(r.Schema.Values) > 0 { - schemaRegexp := patternsToRegex(r.Schema.Values) - if r.Schema.IsExcludes { - schemaPart = "(?!" + schemaRegexp + ").*" - } else { - schemaPart = "(" + schemaRegexp + ")" - } - } - - tablePart := ".*" - if len(r.Table.Values) > 0 { - tableRegexp := patternsToRegex(r.Table.Values) - if r.Table.IsExcludes { - tablePart = "(?!" + tableRegexp + ").*" - } else { - tablePart = "(" + tableRegexp + ")" - } - } - - return catalogPart + `\.` + schemaPart + `\.` + tablePart -} - func globToRegex(pattern string) string { escaped := regexp.QuoteMeta(pattern) escaped = strings.ReplaceAll(escaped, "\\*", ".*") diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go index 5a60220..fabc504 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/pkg/rbac/ranger/ranger.go @@ -52,6 +52,7 @@ func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { } permitions := ar.permitionsByUser[user] + for _, access := range accessList { for _, permition := range permitions.DenyPolicys[access.Action()] { if permition.controlAnAccess(access) { @@ -59,14 +60,20 @@ func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { return false, nil } } + foundAllowPolicy := false for _, permition := range permitions.AllowPolicys[access.Action()] { if permition.controlAnAccess(access) { log.Println("Access allowed by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) - return true, nil + foundAllowPolicy = true + break } } + if !foundAllowPolicy { + log.Println("Access denied by ranger policy", "user", user, "query", query, "action", access.Action(), "resource", access.QualifiedName()) + return false, nil + } } - return false, nil + return true, nil } func (ar *ApacheRanger) startSyncPolicies(ctx context.Context) { diff --git a/pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go b/pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go new file mode 100644 index 0000000..9a88db0 --- /dev/null +++ b/pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go @@ -0,0 +1,165 @@ +package tests + +import ( + "testing" + + "github.com/patterninc/heimdall/pkg/rbac/ranger" +) + +func TestDenyPermissionsForUser(t *testing.T) { + tests := []testCase{ + { + name: "Policy denies select action for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForUser([]string{"select"}), + }, + { + name: "Policy denies insert action for user", + query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForUser([]string{"insert"}), + }, + { + name: "Policy denies update action for user but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForUser([]string{"update"}), + }, + { + name: "Policy denies select and insert actions for user", + query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"select", "insert"}, []string{"all"}), + }, + { + name: "Policy denies all actions for user but exception allows select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"all"}, []string{"select"}), + }, + } + + runTests(t, tests) +} + +func getAllowAllPolicyWithDenyForUser(denyAccess []string) []*ranger.Policy { + return []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range denyAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + }, + } +} + +func getAllowAllPolicyWithDenyAndExceptionForUser(denyAccess, exceptionAccess []string) []*ranger.Policy { + return []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range denyAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + DenyExceptions: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range exceptionAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + }, + } +} diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index 7a44ea1..7a4a4e4 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -9,19 +9,32 @@ import ( ) const ( - serviceName = "test_service" + serviceName = "test_service" + testGroupName = "test_group" + testUserName = "test_user" ) +var ( + testDefaultUsers = map[string]*ranger.User{ + testUserName: {ID: 11, Name: testUserName, GroupIdList: []int64{1}}, + } + testDefaultGroups = map[string]*ranger.Group{ + testGroupName: {ID: 1, Name: testGroupName}, + } +) + +type testCase struct { + name string + query string + username string + expectedResult bool + users map[string]*ranger.User + groups map[string]*ranger.Group + policies []*ranger.Policy +} + func TestRangerPolicyCheck(t *testing.T) { - tests := []struct { - name string - query string - username string - expectedResult bool - users map[string]*ranger.User - groups map[string]*ranger.Group - policies []*ranger.Policy - }{ + tests := []testCase{ { name: "User with direct allow policy", query: "SELECT * FROM default_catalog.public.table1", @@ -41,16 +54,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow select for alice", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -85,16 +98,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow select for group1", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -129,16 +142,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Deny select for charlie", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -192,16 +205,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow select for eve", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -222,16 +235,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Deny select for group3", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -266,12 +279,12 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow show for group4", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, @@ -306,16 +319,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow select for group5 on tables matching regex", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"*"}, IsExcludes: false, }, @@ -350,16 +363,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Deny select for group6 on tables matching regex", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table_*"}, IsExcludes: false, }, @@ -402,16 +415,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow select for group7 excluding user ivan", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -454,16 +467,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Deny select for group8 excluding user judy", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -506,16 +519,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Deny select for group8 excluding user judy", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"public"}, IsExcludes: false, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -566,16 +579,16 @@ func TestRangerPolicyCheck(t *testing.T) { Name: "Allow select for group9 excluding schema 'internal'", PolicyType: 0, PolicyPriority: 1, - Resources: ranger.Resource{ - Catalog: ranger.ResourceField{ + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ Values: []string{"default_catalog"}, IsExcludes: false, }, - Schema: ranger.ResourceField{ + Schema: &ranger.ResourceField{ Values: []string{"internal"}, IsExcludes: true, }, - Table: ranger.ResourceField{ + Table: &ranger.ResourceField{ Values: []string{"table1"}, IsExcludes: false, }, @@ -592,8 +605,313 @@ func TestRangerPolicyCheck(t *testing.T) { }, }, } - for _, tt := range tests { + runTests(t, tests) +} + +// TestResourcesSelection tests the resource selection logic in Ranger policies. +// In this tests users always have all permitions +func TestResourcesSelection(t *testing.T) { + tests := []testCase{ + { + name: "Policy doesn't control the resource, different table name", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "table2"), nil)}, + }, + { + name: "Policy doesn't control the resource, different schema name", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "private", "table1"), nil)}, + }, + { + name: "Policy doesn't control the resource, different catalog name", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("not_default_catalog", "public", "table1"), nil)}, + }, + { + name: "Policy and subpolicy doesn't control the resource, different catalog/table name", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("not_default_catalog", "public", "table1"), createResource("default_catalog", "public", "table2"))}, + }, + { + name: "Policy controls the resource, catalog is regexp", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_*", "public", "table1"), nil)}, + }, + { + name: "Policy controls the resource, schema is regexp", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "p*c", "table1"), nil)}, + }, + { + name: "Policy controls the resource, table is regexp", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "t*l*"), nil)}, + }, + { + name: "Policy controls the resource, table is regexp", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "t*l*"), nil)}, + }, + { + name: "Policy controls the resource, exact match", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "table1"), nil)}, + }, + { + name: "Policy controls the resource, catalog is exclude", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("catalog", "public", "table1", true), nil)}, + }, + { + name: "Policy controls the resource, catalog is exclude regexp", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("catalo*", "public", "table1", true), nil)}, + }, + { + name: "Policy doesn't control the resource, catalog is exclude regexp but match", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("defa*", "public", "table1", true), nil)}, + }, + { + name: "Policy and subpolicy control the resource, catalog is exclude but subpolicy match", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("defa*", "public", "table1", true), createResource("default_catalog", "public", "table1"))}, + }, + { + name: "Policy doesn't control the resource, schema is exclude and match", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForSchema("defa*", "public", "table1", true), nil)}, + }, + { + name: "Policy controls the resource, schema is exclude but not match", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForSchema("defa*", "privat*", "table1", true), nil)}, + }, + { + name: "Policy and subpolicy control the resource, schema is exclude but subpolicy match", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForSchema("defa*", "public", "table1", true), createResource("default_catalog", "public", "table1"))}, + }, + { + name: "Policy doesn't control the resources table is excluded", + query: "SELECT * from default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForTable("defau*", "public", "table1", true), nil)}, + }, + { + name: "Policy does control the resources, table is excluded but doesn't match", + query: "SELECT * from default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForTable("defau*", "public", "table2", true), nil)}, + }, + } + runTests(t, tests) + +} + +func TestAllowPermissionsForUser(t *testing.T) { + tests := []testCase{ + { + name: "Policy allows all actions for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, + }, + { + name: "Policy allows select action for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, + }, + { + name: "Policy allows insert action for user, but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert"})}, + }, + { + name: "Policy allows multiple actions including select for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "select", "update"})}, + }, + { + name: "Policy allows multiple actions excluding select for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "update", "delete"})}, + }, + { + name: "No policy for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{}, + }, + { + name: "Policy allows select but query requires also insert", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, + }, + { + name: "Policy allows all actions", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, + }, + { + name: "Policy many actions and many are required", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"delete", "insert", "select", "update"})}, + }, + { + name: "Policy exclude user from the select action", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"select"})}, + }, + { + name: "Policy exclude user from the insert action, but query is select and insert", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, + }, + { + name: "Policy exclude user from the insert action, but query is select ", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, + }, + } + + runTests(t, tests) + +} + + + +func TestAllowPermissionsForGroups(t *testing.T) { + tests := []testCase{} + + runTests(t, tests) +} + +func TestDenyPermissionsForGroups(t *testing.T) { + tests := []testCase{} + + runTests(t, tests) +} + +func runTests(t *testing.T, tests []testCase) { + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rbac := &ranger.ApacheRanger{ @@ -612,7 +930,181 @@ func TestRangerPolicyCheck(t *testing.T) { } }) } +} + +func createResourceWithExcludeOptionForTable(catalogs, schemas, table string, excludeTable bool) *ranger.Resource { + return &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{catalogs}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{schemas}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{table}, + IsExcludes: excludeTable, + }, + } +} + +func createResourceWithExcludeOptionForSchema(catalog, schema, table string, excludeSchema bool) *ranger.Resource { + return &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{catalog}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{schema}, + IsExcludes: excludeSchema, + }, + Table: &ranger.ResourceField{ + Values: []string{table}, + IsExcludes: false, + }, + } +} + +func createResourceWithExcludeOptionForCatalog(catalogs, schemas, tables string, excludeCatalog bool) *ranger.Resource { + return &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{catalogs}, + IsExcludes: excludeCatalog, + }, + Schema: &ranger.ResourceField{ + Values: []string{schemas}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{tables}, + IsExcludes: false, + }, + } +} + +func createResource(catalogs, schemas, tables string) *ranger.Resource { + return &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{catalogs}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{schemas}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{tables}, + IsExcludes: false, + }, + } +} + +func getAllowAllPolicy(resource *ranger.Resource, additionalResource *ranger.Resource) *ranger.Policy { + var additionalResources []*ranger.Resource + if additionalResource != nil { + additionalResources = []*ranger.Resource{additionalResource} + } + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: resource, + AdditionalResources: additionalResources, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + } +} + +func getDefaultUserAllowPolicy(accessType []string) *ranger.Policy { + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, at := range accessType { + accesses = append(accesses, ranger.Access{Type: at}) + } + return accesses + }(), + }, + }, + } +} +func getDefaultAllActionsUserPolicyWithExcludeForDefaultUser(excludeAccess []string) *ranger.Policy { + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + AllowExceptions: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, ex := range excludeAccess { + accesses = append(accesses, ranger.Access{Type: ex}) + } + return accesses + }(), + }, + }, + } } func getMockRangerClient(users map[string]*ranger.User, groups map[string]*ranger.Group, policies []*ranger.Policy) *mocks.Client { diff --git a/pkg/rbac/ranger/user_sync.go b/pkg/rbac/ranger/user_sync.go index 7d5fd2e..0f3235a 100644 --- a/pkg/rbac/ranger/user_sync.go +++ b/pkg/rbac/ranger/user_sync.go @@ -7,18 +7,19 @@ import ( ) func (r *ApacheRanger) SyncState() error { - users, err := r.Client.GetUsers() + policies, err := r.Client.GetPolicies(r.ServiceName) if err != nil { return err } - groups, err := r.Client.GetGroups() + users, err := r.Client.GetUsers() if err != nil { return err } - policies, err := r.Client.GetPolicies(r.ServiceName) + groups, err := r.Client.GetGroups() if err != nil { return err } + println("Users:", len(users), "Groups:", len(groups), "Policies:", len(policies)) groupByID := map[int64]*Group{} @@ -34,13 +35,13 @@ func (r *ApacheRanger) SyncState() error { } } } - + newPermitionsByUser := map[string]*UserPermitions{} for _, policy := range policies { if !policy.IsEnabled { continue } - if len(policy.Resources.Catalog.Values) == 0 || len(policy.Resources.Schema.Values) == 0 || len(policy.Resources.Table.Values) == 0 { + if policy.Resources == nil || policy.Resources.Catalog == nil || policy.Resources.Schema == nil || policy.Resources.Table == nil { // Skip policies that do not have catalog, schema, and table defined continue } From 885e7ac3155cb135b352cfd7f23aeabf9b9c3f59 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 11 Sep 2025 09:38:31 -0600 Subject: [PATCH 05/14] Small refactoring --- cmd/heimdall/heimdall.go | 2 +- configs/local.yaml | 45 ++++++++++++---- internal/pkg/heimdall/heimdall.go | 86 +++++++++++++++++++------------ pkg/object/cluster/cluster.go | 3 ++ pkg/rbac/ranger/client.go | 28 ++++++++++ pkg/rbac/ranger/policy.go | 1 - pkg/rbac/ranger/ranger.go | 18 +++++-- pkg/rbac/rbac.go | 70 +++++++++++++++++++++++++ pkg/sql/parser/factory/factory.go | 17 ++++++ pkg/sql/parser/sql.go | 4 +- pkg/sql/parser/trino/parser.go | 1 + 11 files changed, 225 insertions(+), 50 deletions(-) create mode 100644 pkg/sql/parser/factory/factory.go diff --git a/cmd/heimdall/heimdall.go b/cmd/heimdall/heimdall.go index 08bef3b..067026a 100644 --- a/cmd/heimdall/heimdall.go +++ b/cmd/heimdall/heimdall.go @@ -29,7 +29,7 @@ var ( func init() { - flag.StringVar(&configFile, `conf`, `/etc/heimdall/heimdall.yaml`, `config file`) + flag.StringVar(&configFile, `conf`, `/Users/ivanhladush/git/heimdall/configs/local.yaml`, `config file`) flag.Parse() } diff --git a/configs/local.yaml b/configs/local.yaml index b420f30..72d26e9 100644 --- a/configs/local.yaml +++ b/configs/local.yaml @@ -1,4 +1,3 @@ - # database settings database: connection_string: "postgres://heimdall:heimdall@postgres:5432/heimdall?sslmode=disable" @@ -8,14 +7,14 @@ pool: size: 5 sleep: 500 -# plugins location -plugin_directory: ./plugins +# # plugins location +# plugin_directory: ./plugins -# auth plugin -auth: - plugin: ./plugins/auth_header.so - context: - header: X-Heimdall-User +# # auth plugin +# auth: +# plugin: ./plugins/auth_header.so +# context: +# header: X-Heimdall-User # supported commands commands: @@ -36,6 +35,34 @@ clusters: status: active version: 0.0.1 description: Just a localhost + rbacs: + - trino + - trino2 tags: - type:localhost - - data:local \ No newline at end of file + - data:local + +rbacs: + - name: trino + type: apache_ranger + service_name: TrinoRanger + sync_interval_in_minutes: 1 + client: + url: http://localhost:6080 + username: admin + password: admin + parser: + type: trino + default_catalog: hive + + - name: trino2 + type: apache_ranger + service_name: TrinoRanger + sync_interval_in_minutes: 1 + client: + url: http://localhost:6080 + username: admin + password: + parser: + type: trino + default_catalog: hive diff --git a/internal/pkg/heimdall/heimdall.go b/internal/pkg/heimdall/heimdall.go index 1db07b2..4d2b9c5 100644 --- a/internal/pkg/heimdall/heimdall.go +++ b/internal/pkg/heimdall/heimdall.go @@ -1,6 +1,7 @@ package heimdall import ( + "context" "fmt" "net/http" "net/http/httputil" @@ -20,6 +21,7 @@ import ( "github.com/patterninc/heimdall/pkg/object/command" "github.com/patterninc/heimdall/pkg/object/job" "github.com/patterninc/heimdall/pkg/plugin" + "github.com/patterninc/heimdall/pkg/rbac" ) const ( @@ -42,6 +44,7 @@ type Heimdall struct { Server *server.Server `yaml:"server,omitempty" json:"server,omitempty"` Commands command.Commands `yaml:"commands,omitempty" json:"commands,omitempty"` Clusters cluster.Clusters `yaml:"clusters,omitempty" json:"clusters,omitempty"` + RBACs rbac.RBACs `yaml:"rbacs,omitempty" json:"rbacs,omitempty"` JobsDirectory string `yaml:"jobs_directory,omitempty" json:"jobs_directory,omitempty"` ArchiveDirectory string `yaml:"archive_directory,omitempty" json:"archive_directory,omitempty"` ResultDirectory string `yaml:"result_directory,omitempty" json:"result_directory,omitempty"` @@ -79,41 +82,48 @@ func (h *Heimdall) Init() error { } h.agentName = fmt.Sprintf("%s-%d", strings.ToLower(hostname), time.Now().UnixMicro()) - // let's load all the plugins - plugins, err := h.loadPlugins() - if err != nil { - return err - } + // // let's load all the plugins + // plugins, err := h.loadPlugins() + // if err != nil { + // return err + // } - h.commandHandlers = make(map[string]plugin.Handler) + // h.commandHandlers = make(map[string]plugin.Handler) // process commands / add default values if missing, write commands to db - for _, c := range h.Commands { - - // set defaults for missing properties - if err := c.Init(); err != nil { - return err - } - - // set command handlers - pluginNew, found := plugins[c.Plugin] - if !found { - return fmt.Errorf(formatErrUnknownPlugin, c.Plugin) - } - - handler, err := pluginNew(c.Context) - if err != nil { - return err - } - h.commandHandlers[c.ID] = handler - - // let's record command in the database - if err := h.commandUpsert(c); err != nil { - return err + // for _, c := range h.Commands { + + // // set defaults for missing properties + // if err := c.Init(); err != nil { + // return err + // } + + // // // set command handlers + // // pluginNew, found := plugins[c.Plugin] + // // if !found { + // // return fmt.Errorf(formatErrUnknownPlugin, c.Plugin) + // // } + + // // handler, err := pluginNew(c.Context) + // // if err != nil { + // // return err + // // } + // // h.commandHandlers[c.ID] = handler + + // // let's record command in the database + // if err := h.commandUpsert(c); err != nil { + // return err + // } + + // } + + rbacsByName := map[string]rbac.RBAC{} + for rbacName, r := range h.RBACs { + if err := r.Init(context.Background()); err != nil { + return fmt.Errorf("failed to init rbac %s: %w", rbacName, err) } - + rbacsByName[rbacName] = r } - // process commands / add default values if missing, write commands to db for _, c := range h.Clusters { @@ -122,11 +132,19 @@ func (h *Heimdall) Init() error { return err } - // let's record command in the database - if err := h.clusterUpsert(c); err != nil { - return err + // // let's record command in the database + // if err := h.clusterUpsert(c); err != nil { + // return err + // } + if len(c.RBACNames) > 0 { + for _, rbacName := range c.RBACNames { + r, found := rbacsByName[rbacName] + if !found { + return fmt.Errorf("failed to find rbac %s for cluster %s", rbacName, c.Name) + } + c.RBACs = append(c.RBACs, r) + } } - } // start janitor diff --git a/pkg/object/cluster/cluster.go b/pkg/object/cluster/cluster.go index 23956e7..f4d7aec 100644 --- a/pkg/object/cluster/cluster.go +++ b/pkg/object/cluster/cluster.go @@ -5,6 +5,7 @@ import ( "github.com/patterninc/heimdall/pkg/object" "github.com/patterninc/heimdall/pkg/object/status" + "github.com/patterninc/heimdall/pkg/rbac" ) var ( @@ -14,6 +15,8 @@ var ( type Cluster struct { object.Object `yaml:",inline" json:",inline"` Status status.Status `yaml:"status,omitempty" json:"status,omitempty"` + RBACNames []string `yaml:"rbacs,omitempty" json:"rbacs,omitempty"` + RBACs []rbac.RBAC `yaml:"-" json:"-"` } type Clusters map[string]*Cluster diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go index 4b9ddf5..fe3f20d 100644 --- a/pkg/rbac/ranger/client.go +++ b/pkg/rbac/ranger/client.go @@ -9,6 +9,8 @@ import ( "net/http" "strings" "time" + + "gopkg.in/yaml.v3" ) const ( @@ -46,6 +48,32 @@ type getResponse struct { //go:generate go run github.com/vektra/mockery/v2@v2.53.4 --name=Client --output=./mocks --outpkg=mocks +type ClientWrapper struct { + Client Client +} + +func (aw *ClientWrapper) UnmarshalYAML(value *yaml.Node) error { + var cl client + if err := value.Decode(&cl); err != nil { + return err + } + aw.Client = &cl + cl.client = &http.Client{} + return nil +} + +func (cw *ClientWrapper) GetUsers() (map[string]*User, error) { + return cw.Client.GetUsers() +} + +func (cw *ClientWrapper) GetGroups() (map[string]*Group, error) { + return cw.Client.GetGroups() +} + +func (cw *ClientWrapper) GetPolicies(serviceName string) ([]*Policy, error) { + return cw.Client.GetPolicies(serviceName) +} + type Client interface { GetUsers() (map[string]*User, error) GetGroups() (map[string]*Group, error) diff --git a/pkg/rbac/ranger/policy.go b/pkg/rbac/ranger/policy.go index 931d1f2..61b1a84 100644 --- a/pkg/rbac/ranger/policy.go +++ b/pkg/rbac/ranger/policy.go @@ -150,7 +150,6 @@ func (p *Policy) controlTableAccess(a *parser.TableAccess) bool { continue } } - return true } return false diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go index fabc504..bc352de 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/pkg/rbac/ranger/ranger.go @@ -10,12 +10,18 @@ import ( ) type ApacheRanger struct { - Name string `yaml:"name,omitempty" json:"name,omitempty"` - ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` - Client Client - SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` + Client ClientWrapper `yaml:"client,omitempty" json:"client,omitempty"` + SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` AccessReceiver parser.AccessReceiver permitionsByUser map[string]*UserPermitions + Parser ParserConfig `yaml:"parser,omitempty" json:"parser,omitempty"` +} + +type ParserConfig struct { + Type string `yaml:"type,omitempty" json:"type,omitempty"` + DefaultCatalog string `yaml:"default_catalog,omitempty" json:"default_catalog,omitempty"` } type PermitionStatus int @@ -98,3 +104,7 @@ func (ar *ApacheRanger) startSyncPolicies(ctx context.Context) { } }() } + +func (r *ApacheRanger) GetName() string { + return r.Name +} diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index a037ee7..0e73b94 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -2,9 +2,79 @@ package rbac import ( "context" + "errors" + "fmt" + + "github.com/patterninc/heimdall/pkg/rbac/ranger" + parserFactory "github.com/patterninc/heimdall/pkg/sql/parser/factory" + "gopkg.in/yaml.v3" +) + +var ( + ErrRBACIDsAreNotUnique = errors.New("rbac IDs are not unique") ) type RBAC interface { Init(ctx context.Context) error HasAccess(user string, query string) (bool, error) + GetName() string +} + +type RBACs map[string]RBAC + +func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { + + var temp RBACConfig + + if err := unmarshal(&temp); err != nil { + return err + } + + items := make(map[string]RBAC) + + for _, t := range temp.RBAC { + items[t.GetName()] = t + } + + if len(temp.RBAC) != len(items) { + return ErrRBACIDsAreNotUnique + } + + *c = items + + return nil + +} + +type RBACConfig struct { + RBAC []RBAC +} + +// Implements custom unmarshaling based on `type` field in YAML +func (c *RBACConfig) UnmarshalYAML(value *yaml.Node) error { + for _, value := range value.Content { + var probe struct { + Type string `yaml:"type"` + } + if err := value.Decode(&probe); err != nil { + return err + } + + switch probe.Type { + case "apache_ranger": + var r ranger.ApacheRanger + if err := value.Decode(&r); err != nil { + return err + } + c.RBAC = append(c.RBAC, &r) + parser, err := parserFactory.CreateParserByType(r.Parser.Type, r.Parser.DefaultCatalog) + if err != nil { + return err + } + r.AccessReceiver = parser + default: + return fmt.Errorf("unknown RBAC type: %s", probe.Type) + } + } + return nil } diff --git a/pkg/sql/parser/factory/factory.go b/pkg/sql/parser/factory/factory.go new file mode 100644 index 0000000..ef44a8a --- /dev/null +++ b/pkg/sql/parser/factory/factory.go @@ -0,0 +1,17 @@ +package factory + +import ( + "fmt" + + "github.com/patterninc/heimdall/pkg/sql/parser" + "github.com/patterninc/heimdall/pkg/sql/parser/trino" +) + +func CreateParserByType(typ string, defaultCatalog string) (parser.AccessReceiver, error) { + switch typ { + case "trino": + return trino.NewTrinoAccessReceiver(defaultCatalog), nil + default: + return nil, fmt.Errorf("unknown parser type: %s", typ) + } +} diff --git a/pkg/sql/parser/sql.go b/pkg/sql/parser/sql.go index c8c3b2b..3ce7a3b 100644 --- a/pkg/sql/parser/sql.go +++ b/pkg/sql/parser/sql.go @@ -1,6 +1,8 @@ package parser -import "fmt" +import ( + "fmt" +) type AccessKind int diff --git a/pkg/sql/parser/trino/parser.go b/pkg/sql/parser/trino/parser.go index 006e3e3..d3171fe 100644 --- a/pkg/sql/parser/trino/parser.go +++ b/pkg/sql/parser/trino/parser.go @@ -30,3 +30,4 @@ func (t *TrinoAccessReceiver) ParseAccess(sql string) ([]parser.Access, error) { return col.collected, nil } + From b5a9861a53653d7a7e3b592b951d06519ea756fc Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 11 Sep 2025 09:38:55 -0600 Subject: [PATCH 06/14] small refactoring --- internal/pkg/heimdall/heimdall.go | 62 +++++------ pkg/rbac/main/main.go | 24 ----- pkg/rbac/ranger/client.go | 92 +++++++--------- pkg/rbac/ranger/policy.go | 173 ++++++++++++------------------ pkg/rbac/ranger/ranger.go | 87 +++++++++++++-- pkg/rbac/ranger/user_sync.go | 81 -------------- pkg/rbac/rbac.go | 12 +-- pkg/sql/parser/factory/factory.go | 8 +- 8 files changed, 234 insertions(+), 305 deletions(-) delete mode 100644 pkg/rbac/main/main.go delete mode 100644 pkg/rbac/ranger/user_sync.go diff --git a/internal/pkg/heimdall/heimdall.go b/internal/pkg/heimdall/heimdall.go index 4d2b9c5..e655ba4 100644 --- a/internal/pkg/heimdall/heimdall.go +++ b/internal/pkg/heimdall/heimdall.go @@ -82,40 +82,40 @@ func (h *Heimdall) Init() error { } h.agentName = fmt.Sprintf("%s-%d", strings.ToLower(hostname), time.Now().UnixMicro()) - // // let's load all the plugins - // plugins, err := h.loadPlugins() - // if err != nil { - // return err - // } + // let's load all the plugins + plugins, err := h.loadPlugins() + if err != nil { + return err + } - // h.commandHandlers = make(map[string]plugin.Handler) + h.commandHandlers = make(map[string]plugin.Handler) // process commands / add default values if missing, write commands to db - // for _, c := range h.Commands { - - // // set defaults for missing properties - // if err := c.Init(); err != nil { - // return err - // } - - // // // set command handlers - // // pluginNew, found := plugins[c.Plugin] - // // if !found { - // // return fmt.Errorf(formatErrUnknownPlugin, c.Plugin) - // // } - - // // handler, err := pluginNew(c.Context) - // // if err != nil { - // // return err - // // } - // // h.commandHandlers[c.ID] = handler - - // // let's record command in the database - // if err := h.commandUpsert(c); err != nil { - // return err - // } - - // } + for _, c := range h.Commands { + + // set defaults for missing properties + if err := c.Init(); err != nil { + return err + } + + // set command handlers + pluginNew, found := plugins[c.Plugin] + if !found { + return fmt.Errorf(formatErrUnknownPlugin, c.Plugin) + } + + handler, err := pluginNew(c.Context) + if err != nil { + return err + } + h.commandHandlers[c.ID] = handler + + // let's record command in the database + if err := h.commandUpsert(c); err != nil { + return err + } + + } rbacsByName := map[string]rbac.RBAC{} for rbacName, r := range h.RBACs { diff --git a/pkg/rbac/main/main.go b/pkg/rbac/main/main.go deleted file mode 100644 index f559185..0000000 --- a/pkg/rbac/main/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "github.com/patterninc/heimdall/pkg/rbac/ranger" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" -) - -func main() { - r := &ranger.ApacheRanger{ - Name: "LocalRanger", - Client: ranger.NewClient("http://localhost:6080", "admin", ""), - ServiceName: "TrinoRanger", - AccessReceiver: trino.NewTrinoAccessReceiver("glue_catalog"), - SyncIntervalInMinutes: 5, - } - err := r.SyncState() - if err != nil { - panic(err) - } - - println(r.HasAccess("ihladush", "SELECT * FROM test.test_table")) - println(r.HasAccess("ivan.hladush@pattern.com", "SELECT * FROM test.test_table")) - -} diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go index fe3f20d..791e62a 100644 --- a/pkg/rbac/ranger/client.go +++ b/pkg/rbac/ranger/client.go @@ -19,34 +19,12 @@ const ( getGroupsEndpoint = `/service/xusers/groups` ) -type User struct { - ID int64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - EmailAddress string `json:"emailAddress,omitempty"` - UserRoleList []string `json:"userRoleList,omitempty"` - Password string `json:"password,omitempty"` - SyncSource string `json:"syncSource,omitempty"` - GroupIdList []int64 `json:"groupIdList,omitempty"` -} - -type Group struct { - ID int64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - SyncSource string `json:"syncSource,omitempty"` -} - -type getResponse struct { - PageSize int `json:"pageSize"` - StartIndex int `json:"startIndex"` - ResultSize int `json:"resultSize"` - VXUsers []*User `json:"vXUsers,omitempty"` - VXGroups []*Group `json:"vXGroups,omitempty"` -} - //go:generate go run github.com/vektra/mockery/v2@v2.53.4 --name=Client --output=./mocks --outpkg=mocks +type Client interface { + GetUsers() (map[string]*User, error) + GetGroups() (map[string]*Group, error) + GetPolicies(serviceName string) ([]*Policy, error) +} type ClientWrapper struct { Client Client @@ -74,16 +52,37 @@ func (cw *ClientWrapper) GetPolicies(serviceName string) ([]*Policy, error) { return cw.Client.GetPolicies(serviceName) } -type Client interface { - GetUsers() (map[string]*User, error) - GetGroups() (map[string]*Group, error) - GetPolicies(serviceName string) ([]*Policy, error) +type User struct { + ID int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + EmailAddress string `json:"emailAddress,omitempty"` + UserRoleList []string `json:"userRoleList,omitempty"` + Password string `json:"password,omitempty"` + SyncSource string `json:"syncSource,omitempty"` + GroupIdList []int64 `json:"groupIdList,omitempty"` +} + +type Group struct { + ID int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + SyncSource string `json:"syncSource,omitempty"` +} + +type getResponse struct { + PageSize int `json:"pageSize"` + StartIndex int `json:"startIndex"` + ResultSize int `json:"resultSize"` + VXUsers []*User `json:"vXUsers,omitempty"` + VXGroups []*Group `json:"vXGroups,omitempty"` } type client struct { - URL string `yaml:"url" json:"url" omitempty"` - Username string `yaml:"username" json:"username" omitempty"` - Password string `yaml:"password" json:"password" omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + Password string `yaml:"password,omitempty" json:"password,omitempty"` client *http.Client } @@ -95,6 +94,7 @@ func NewClient(url, username, password string) Client { client: &http.Client{}, } } + func (c *client) GetUsers() (map[string]*User, error) { responses, err := c.executeBatchRequest(http.MethodGet, getUsersEndpoint) @@ -119,7 +119,6 @@ func (c *client) GetUsers() (map[string]*User, error) { } func (c *client) GetGroups() (map[string]*Group, error) { - responses, err := c.executeBatchRequest(http.MethodGet, getGroupsEndpoint) if err != nil { return nil, err @@ -131,12 +130,10 @@ func (c *client) GetGroups() (map[string]*Group, error) { for _, group := range resp.VXGroups { groupsMap[group.Name] = group } - } log.Printf("Number of Ranger Groups pulled: %d\n", len(groupsMap)) return groupsMap, nil - } func (c *client) GetPolicies(serviceName string) ([]*Policy, error) { @@ -146,12 +143,6 @@ func (c *client) GetPolicies(serviceName string) ([]*Policy, error) { } func (c *client) createRequest(method, endpoint string, reqBody interface{}) (*http.Request, error) { - - // Ensure client exists - if c.client == nil { - c.client = &http.Client{} - } - var jsonBody []byte var err error @@ -177,14 +168,13 @@ func (c *client) createRequest(method, endpoint string, reqBody interface{}) (*h } -func (r *client) executeRequest(method string, endpoint string, v interface{}, reqBody interface{}) error { - - req, err := r.createRequest(method, endpoint, reqBody) +func (c *client) executeRequest(method string, endpoint string, v interface{}, reqBody interface{}) error { + req, err := c.createRequest(method, endpoint, reqBody) if err != nil { return err } - resp, err := r.client.Do(req) + resp, err := c.client.Do(req) if err != nil { return err } @@ -206,12 +196,10 @@ func (r *client) executeRequest(method string, endpoint string, v interface{}, r } return nil - } // executeBatchRequest performs paginated API requests and returns all aggregated results -func (r *client) executeBatchRequest(method string, endpoint string) ([]getResponse, error) { - +func (c *client) executeBatchRequest(method string, endpoint string) ([]getResponse, error) { results := make([]getResponse, 500) pageSize := 200 startIndex := 0 @@ -222,7 +210,7 @@ func (r *client) executeBatchRequest(method string, endpoint string) ([]getRespo // Marshall into generic get getResponse := &getResponse{} - if err := r.executeRequest(method, batchEndpoint, getResponse, nil); err != nil { + if err := c.executeRequest(method, batchEndpoint, getResponse, nil); err != nil { return nil, err } @@ -236,9 +224,7 @@ func (r *client) executeBatchRequest(method string, endpoint string) ([]getRespo } startIndex += pageSize - } return results, nil - } diff --git a/pkg/rbac/ranger/policy.go b/pkg/rbac/ranger/policy.go index 61b1a84..52221db 100644 --- a/pkg/rbac/ranger/policy.go +++ b/pkg/rbac/ranger/policy.go @@ -1,6 +1,7 @@ package ranger import ( + "log" "regexp" "strings" @@ -50,18 +51,19 @@ var ( ) type Policy struct { - ID int `json:"id"` - GUID string `json:"guid"` - IsEnabled bool `json:"isEnabled"` - Version int `json:"version"` - Service string `json:"service"` - Name string `json:"name"` - PolicyType int `json:"policyType"` - PolicyPriority int `json:"policyPriority"` - Description string `json:"description"` - IsAuditEnabled bool `json:"isAuditEnabled"` - Resources *Resource `json:"resources"` - AdditionalResources []*Resource `json:"additionalResources"` + ID int `json:"id"` + GUID string `json:"guid"` + IsEnabled bool `json:"isEnabled"` + Version int `json:"version"` + Service string `json:"service"` + Name string `json:"name"` + PolicyType int `json:"policyType"` + PolicyPriority int `json:"policyPriority"` + Description string `json:"description"` + IsAuditEnabled bool `json:"isAuditEnabled"` + Resources *Resource `json:"resources"` + AdditionalResources []*Resource `json:"additionalResources"` + AllResources []*Resource PolicyItems []PolicyItem `json:"policyItems"` DenyPolicyItems []PolicyItem `json:"denyPolicyItems"` AllowExceptions []PolicyItem `json:"allowExceptions"` @@ -69,12 +71,6 @@ type Policy struct { ServiceType string `json:"serviceType"` } -type ResourceField struct { - Values []string `json:"values"` - IsExcludes bool `json:"isExcludes"` - regexp *regexp.Regexp -} - type Resource struct { Schema *ResourceField `json:"schema,omitempty"` Catalog *ResourceField `json:"catalog,omitempty"` @@ -82,6 +78,12 @@ type Resource struct { Column *ResourceField `json:"column,omitempty"` } +type ResourceField struct { + Values []string `json:"values"` + IsExcludes bool `json:"isExcludes"` + regexp *regexp.Regexp +} + type Access struct { Type string `json:"type"` } @@ -99,7 +101,8 @@ type ControlledActions struct { } func (p *Policy) init() error { - for _, v := range append([]*Resource{p.Resources}, p.AdditionalResources...) { + p.AllResources = append([]*Resource{p.Resources}, p.AdditionalResources...) + for _, v := range p.AllResources { if len(v.Catalog.Values) != 0 { v.Catalog.regexp = regexp.MustCompile("^(" + patternsToRegex(v.Catalog.Values) + ")$") } @@ -113,43 +116,31 @@ func (p *Policy) init() error { return nil } -func (p *Policy) controlAnAccess(access parser.Access) bool { +func (p *Policy) doesControlAnAccess(access parser.Access) bool { switch a := access.(type) { case *parser.TableAccess: - return p.controlTableAccess(a) + return p.doesControlTableAccess(a) } return false } -func (p *Policy) controlTableAccess(a *parser.TableAccess) bool { - for _, v := range append([]*Resource{p.Resources}, p.AdditionalResources...) { - if len(v.Catalog.Values) != 0 { - matchCatalog := v.Catalog.regexp.MatchString(a.Catalog) - if matchCatalog && v.Catalog.IsExcludes { - continue - } - if !matchCatalog && !v.Catalog.IsExcludes { - continue - } +func (p *Policy) doesControlTableAccess(a *parser.TableAccess) bool { + for _, v := range p.AllResources { + match := v.Catalog.regexp.MatchString(a.Catalog) + if match == v.Catalog.IsExcludes { + continue } - if len(v.Schema.Values) != 0 { - matchSchema := v.Schema.regexp.MatchString(a.Schema) - if matchSchema && v.Schema.IsExcludes { - continue - } - if !matchSchema && !v.Schema.IsExcludes { - continue - } + + match = v.Schema.regexp.MatchString(a.Schema) + if match == v.Schema.IsExcludes { + continue } - if len(v.Table.Values) != 0 { - matchTable := v.Table.regexp.MatchString(a.Table) - if matchTable && v.Table.IsExcludes { - continue - } - if !matchTable && !v.Table.IsExcludes { - continue - } + + match = v.Table.regexp.MatchString(a.Table) + if match == v.Table.IsExcludes { + continue } + return true } return false @@ -157,56 +148,33 @@ func (p *Policy) controlTableAccess(a *parser.TableAccess) bool { func (p *Policy) getControlledActions(usersByGroup map[string][]string) ControlledActions { return ControlledActions{ - allowedActionsByUser: p.getAllAllowPolicyByUser(usersByGroup), - deniedActionsByUser: p.getAllDenyPoliciesByUser(usersByGroup), + allowedActionsByUser: p.getAllPolicyByUser(p.PolicyItems, p.AllowExceptions, usersByGroup), + deniedActionsByUser: p.getAllPolicyByUser(p.DenyPolicyItems, p.DenyExceptions, usersByGroup), } } -func (p *Policy) getAllAllowPolicyByUser(usersByGroup map[string][]string) map[string][]parser.Action { - allowPoliciesItem := policyItemsToActionsByUser(p.PolicyItems, usersByGroup) - excludeAllowPolicyItems := policyItemsToActionsByUser(p.AllowExceptions, usersByGroup) +func (p *Policy) getAllPolicyByUser( + items []PolicyItem, + exceptions []PolicyItem, + usersByGroup map[string][]string, +) map[string][]parser.Action { + policiesItem := policyItemsToActionsByUser(items, usersByGroup) + exceptionsItem := policyItemsToActionsByUser(exceptions, usersByGroup) - for user, actions := range excludeAllowPolicyItems { - if _, ok := allowPoliciesItem[user]; !ok { + for user, actions := range exceptionsItem { + if _, ok := policiesItem[user]; !ok { continue } for action := range actions { - delete(allowPoliciesItem[user], action) + delete(policiesItem[user], action) } - if len(allowPoliciesItem[user]) == 0 { - delete(allowPoliciesItem, user) + if len(policiesItem[user]) == 0 { + delete(policiesItem, user) } } result := map[string][]parser.Action{} - for user, actionsMap := range allowPoliciesItem { - actions := make([]parser.Action, 0, len(actionsMap)) - for action := range actionsMap { - actions = append(actions, action) - } - result[user] = actions - } - return result -} - -func (p *Policy) getAllDenyPoliciesByUser(usersByGroup map[string][]string) map[string][]parser.Action { - denyPoliciesItem := policyItemsToActionsByUser(p.DenyPolicyItems, usersByGroup) - excludeDenyPolicyItems := policyItemsToActionsByUser(p.DenyExceptions, usersByGroup) - - for user, actions := range excludeDenyPolicyItems { - if _, ok := denyPoliciesItem[user]; !ok { - continue - } - for action := range actions { - delete(denyPoliciesItem[user], action) - } - if len(denyPoliciesItem[user]) == 0 { - delete(denyPoliciesItem, user) - } - } - - result := map[string][]parser.Action{} - for user, actionsMap := range denyPoliciesItem { + for user, actionsMap := range policiesItem { actions := make([]parser.Action, 0, len(actionsMap)) for action := range actionsMap { actions = append(actions, action) @@ -230,6 +198,7 @@ func patternsToRegex(patterns []string) string { } return strings.Join(regexes, "|") } + func (p *PolicyItem) getPermissions() []parser.Action { if p.Actions != nil { return p.Actions @@ -244,6 +213,8 @@ func (p *PolicyItem) getPermissions() []parser.Action { if action, ok := actionByName[accessType]; ok { p.Actions = append(p.Actions, action) continue + } else { + log.Println("Unknown action type in ranger policy:", accessType) } } return p.Actions @@ -254,30 +225,26 @@ func policyItemsToActionsByUser(items []PolicyItem, usersByGroup map[string][]st permissions := make(map[string]map[parser.Action]struct{}) for _, item := range items { + actions := item.getPermissions() for _, user := range item.Users { - user = strings.ToLower(user) - if _, ok := permissions[user]; !ok { - permissions[user] = make(map[parser.Action]struct{}) - } - for _, action := range item.getPermissions() { - permissions[user][action] = struct{}{} - } + addActionsToPermissions(permissions, user, actions) } for _, group := range item.Groups { - users, ok := usersByGroup[group] - if !ok { - continue - } - for _, user := range users { - if _, ok := permissions[user]; !ok { - permissions[user] = make(map[parser.Action]struct{}) - } - for _, action := range item.getPermissions() { - permissions[user][action] = struct{}{} - } + for _, user := range usersByGroup[group] { + addActionsToPermissions(permissions, user, actions) } } } return permissions } + +func addActionsToPermissions(permissions map[string]map[parser.Action]struct{}, user string, actions []parser.Action) { + user = strings.ToLower(user) + if _, ok := permissions[user]; !ok { + permissions[user] = make(map[parser.Action]struct{}) + } + for _, action := range actions { + permissions[user][action] = struct{}{} + } +} diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go index bc352de..8771023 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/pkg/rbac/ranger/ranger.go @@ -61,14 +61,14 @@ func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { for _, access := range accessList { for _, permition := range permitions.DenyPolicys[access.Action()] { - if permition.controlAnAccess(access) { + if permition.doesControlAnAccess(access) { log.Println("Access denied by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) return false, nil } } foundAllowPolicy := false for _, permition := range permitions.AllowPolicys[access.Action()] { - if permition.controlAnAccess(access) { + if permition.doesControlAnAccess(access) { log.Println("Access allowed by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) foundAllowPolicy = true break @@ -82,6 +82,85 @@ func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { return true, nil } +func (r *ApacheRanger) GetName() string { + return r.Name +} + +func (r *ApacheRanger) SyncState() error { + policies, err := r.Client.GetPolicies(r.ServiceName) + if err != nil { + return err + } + users, err := r.Client.GetUsers() + if err != nil { + return err + } + groups, err := r.Client.GetGroups() + if err != nil { + return err + } + + log.Println("Users:", len(users), "Groups:", len(groups), "Policies:", len(policies)) + + groupByID := map[int64]*Group{} + usersByGroup := map[string][]string{} + for _, group := range groups { + groupByID[group.ID] = group + } + + for _, user := range users { + for _, gid := range user.GroupIdList { + if group, ok := groupByID[gid]; ok { + usersByGroup[group.Name] = append(usersByGroup[group.Name], user.Name) + } + } + } + + newPermitionsByUser := map[string]*UserPermitions{} + for _, policy := range policies { + if !policy.IsEnabled { + continue + } + if policy.Resources == nil || policy.Resources.Catalog == nil || policy.Resources.Schema == nil || policy.Resources.Table == nil { + // Skip policies that do not have catalog, schema, or table defined + continue + } + + if err := policy.init(); err != nil { + log.Println("Error initializing policy:", err) + return err + } + + controlledActions := policy.getControlledActions(usersByGroup) + for userName, actions := range controlledActions.allowedActionsByUser { + if _, ok := newPermitionsByUser[userName]; !ok { + newPermitionsByUser[userName] = &UserPermitions{ + AllowPolicys: map[parser.Action][]*Policy{}, + DenyPolicys: map[parser.Action][]*Policy{}, + } + } + for _, action := range actions { + newPermitionsByUser[userName].AllowPolicys[action] = append(newPermitionsByUser[userName].AllowPolicys[action], policy) + } + } + for userName, actions := range controlledActions.deniedActionsByUser { + if _, ok := newPermitionsByUser[userName]; !ok { + newPermitionsByUser[userName] = &UserPermitions{ + AllowPolicys: map[parser.Action][]*Policy{}, + DenyPolicys: map[parser.Action][]*Policy{}, + } + } + for _, action := range actions { + newPermitionsByUser[userName].DenyPolicys[action] = append(newPermitionsByUser[userName].DenyPolicys[action], policy) + } + } + } + + r.permitionsByUser = newPermitionsByUser + log.Println("Syncing users and groups from Apache Ranger for service:", r.ServiceName) + return nil +} + func (ar *ApacheRanger) startSyncPolicies(ctx context.Context) { go func() { ctx, cancel := context.WithCancel(ctx) @@ -104,7 +183,3 @@ func (ar *ApacheRanger) startSyncPolicies(ctx context.Context) { } }() } - -func (r *ApacheRanger) GetName() string { - return r.Name -} diff --git a/pkg/rbac/ranger/user_sync.go b/pkg/rbac/ranger/user_sync.go deleted file mode 100644 index 0f3235a..0000000 --- a/pkg/rbac/ranger/user_sync.go +++ /dev/null @@ -1,81 +0,0 @@ -package ranger - -import ( - "log" - - "github.com/patterninc/heimdall/pkg/sql/parser" -) - -func (r *ApacheRanger) SyncState() error { - policies, err := r.Client.GetPolicies(r.ServiceName) - if err != nil { - return err - } - users, err := r.Client.GetUsers() - if err != nil { - return err - } - groups, err := r.Client.GetGroups() - if err != nil { - return err - } - - println("Users:", len(users), "Groups:", len(groups), "Policies:", len(policies)) - - groupByID := map[int64]*Group{} - usersByGroup := map[string][]string{} - for _, group := range groups { - groupByID[group.ID] = group - } - - for _, user := range users { - for _, gid := range user.GroupIdList { - if group, ok := groupByID[gid]; ok { - usersByGroup[group.Name] = append(usersByGroup[group.Name], user.Name) - } - } - } - - newPermitionsByUser := map[string]*UserPermitions{} - for _, policy := range policies { - if !policy.IsEnabled { - continue - } - if policy.Resources == nil || policy.Resources.Catalog == nil || policy.Resources.Schema == nil || policy.Resources.Table == nil { - // Skip policies that do not have catalog, schema, and table defined - continue - } - - if err := policy.init(); err != nil { - log.Println("Error initializing policy:", err) - return err - } - controlledActions := policy.getControlledActions(usersByGroup) - for userName, actions := range controlledActions.allowedActionsByUser { - if _, ok := newPermitionsByUser[userName]; !ok { - newPermitionsByUser[userName] = &UserPermitions{ - AllowPolicys: map[parser.Action][]*Policy{}, - DenyPolicys: map[parser.Action][]*Policy{}, - } - } - for _, action := range actions { - newPermitionsByUser[userName].AllowPolicys[action] = append(newPermitionsByUser[userName].AllowPolicys[action], policy) - } - } - for userName, actions := range controlledActions.deniedActionsByUser { - if _, ok := newPermitionsByUser[userName]; !ok { - newPermitionsByUser[userName] = &UserPermitions{ - AllowPolicys: map[parser.Action][]*Policy{}, - DenyPolicys: map[parser.Action][]*Policy{}, - } - } - for _, action := range actions { - newPermitionsByUser[userName].DenyPolicys[action] = append(newPermitionsByUser[userName].DenyPolicys[action], policy) - } - } - } - - r.permitionsByUser = newPermitionsByUser - log.Println("Syncing users and groups from Apache Ranger for service:", r.ServiceName) - return nil -} diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 0e73b94..7ed1a79 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -22,9 +22,13 @@ type RBAC interface { type RBACs map[string]RBAC +type RBACConfigs struct { + RBAC []RBAC +} + func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { - var temp RBACConfig + var temp RBACConfigs if err := unmarshal(&temp); err != nil { return err @@ -46,12 +50,8 @@ func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { } -type RBACConfig struct { - RBAC []RBAC -} - // Implements custom unmarshaling based on `type` field in YAML -func (c *RBACConfig) UnmarshalYAML(value *yaml.Node) error { +func (c *RBACConfigs) UnmarshalYAML(value *yaml.Node) error { for _, value := range value.Content { var probe struct { Type string `yaml:"type"` diff --git a/pkg/sql/parser/factory/factory.go b/pkg/sql/parser/factory/factory.go index ef44a8a..3816bcb 100644 --- a/pkg/sql/parser/factory/factory.go +++ b/pkg/sql/parser/factory/factory.go @@ -7,9 +7,15 @@ import ( "github.com/patterninc/heimdall/pkg/sql/parser/trino" ) +type ParserType string + +const ( + ParserTypeTrino ParserType = "trino" +) + func CreateParserByType(typ string, defaultCatalog string) (parser.AccessReceiver, error) { switch typ { - case "trino": + case string(ParserTypeTrino): return trino.NewTrinoAccessReceiver(defaultCatalog), nil default: return nil, fmt.Errorf("unknown parser type: %s", typ) From dc970b57db84a1cea75e9cd526efc1510a2b8d11 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 11 Sep 2025 10:01:29 -0600 Subject: [PATCH 07/14] Add more tests --- internal/pkg/object/command/trino/client.go | 11 +- internal/pkg/object/command/trino/trino.go | 36 +- pkg/rbac/ranger/tests/group_policy_test.go | 210 ++++++++++ .../tests/ranger_deny_user_policy_test.go | 165 -------- .../ranger/tests/ranger_policy_check_test.go | 215 +---------- pkg/rbac/ranger/tests/user_policy_test.go | 363 ++++++++++++++++++ 6 files changed, 611 insertions(+), 389 deletions(-) create mode 100644 pkg/rbac/ranger/tests/group_policy_test.go delete mode 100644 pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go create mode 100644 pkg/rbac/ranger/tests/user_policy_test.go diff --git a/internal/pkg/object/command/trino/client.go b/internal/pkg/object/command/trino/client.go index e8e1f4c..9443974 100644 --- a/internal/pkg/object/command/trino/client.go +++ b/internal/pkg/object/command/trino/client.go @@ -45,7 +45,7 @@ type response struct { Error map[string]any `json:"error"` } -func newRequest(r *plugin.Runtime, j *job.Job, c *cluster.Cluster) (*request, error) { +func newRequest(r *plugin.Runtime, j *job.Job, c *cluster.Cluster, jobCtx *jobContext) (*request, error) { // get cluster context clusterCtx := &clusterContext{} @@ -55,15 +55,6 @@ func newRequest(r *plugin.Runtime, j *job.Job, c *cluster.Cluster) (*request, er } } - // get job context - jobCtx := &jobContext{} - if j.Context != nil { - if err := j.Context.Unmarshal(jobCtx); err != nil { - return nil, err - } - } - jobCtx.Query = normalizeTrinoQuery(jobCtx.Query) - // form context for trino request req := &request{ endpoint: clusterCtx.Endpoint, diff --git a/internal/pkg/object/command/trino/trino.go b/internal/pkg/object/command/trino/trino.go index f54f16f..79dc3c8 100644 --- a/internal/pkg/object/command/trino/trino.go +++ b/internal/pkg/object/command/trino/trino.go @@ -2,6 +2,7 @@ package trino import ( "fmt" + "log" "time" "github.com/patterninc/heimdall/pkg/context" @@ -47,8 +48,21 @@ func New(ctx *context.Context) (plugin.Handler, error) { func (t *commandContext) handler(r *plugin.Runtime, j *job.Job, c *cluster.Cluster) error { + // get job context + jobCtx := &jobContext{} + if j.Context != nil { + if err := j.Context.Unmarshal(jobCtx); err != nil { + return err + } + } + jobCtx.Query = normalizeTrinoQuery(jobCtx.Query) + + if !canQueryBeExecuted(jobCtx.Query, j.User, c) { + log.Printf("user %s is not allowed to run the query", j.User) + // todo add metrics here and eventually enable in prod + } // let's submit our query to trino - req, err := newRequest(r, j, c) + req, err := newRequest(r, j, c, jobCtx) if err != nil { return err } @@ -72,3 +86,23 @@ func (t *commandContext) handler(r *plugin.Runtime, j *job.Job, c *cluster.Clust return nil } + +func canQueryBeExecuted(query, user string, c *cluster.Cluster) bool { + // todo add metrics for time spent here + if query == `` { + return false + } + + for _, rbac := range c.RBACs { + allowed, err := rbac.HasAccess(user, query) + if err != nil { + log.Printf("failed to check rbac: %w", err) + return false + } + if !allowed { + log.Printf("user %s is not allowed to run the query", user) + return false + } + } + return true +} diff --git a/pkg/rbac/ranger/tests/group_policy_test.go b/pkg/rbac/ranger/tests/group_policy_test.go new file mode 100644 index 0000000..c841d54 --- /dev/null +++ b/pkg/rbac/ranger/tests/group_policy_test.go @@ -0,0 +1,210 @@ +package tests + +import ( + "testing" + + "github.com/patterninc/heimdall/pkg/rbac/ranger" +) + +func TestAllowPermissionsForGroups(t *testing.T) { + tests := []testCase{ + { + name: "Policy allows all actions for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"all"})}, + }, + { + name: "Policy allows select action for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"select"})}, + }, + { + name: "Policy allows insert action for group, but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"insert"})}, + }, + { + name: "Policy allows multiple actions including select for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"insert", "select", "update"})}, + }, + { + name: "Policy allows multiple actions excluding select for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"insert", "update", "delete"})}, + }, + { + name: "No policy for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{}, + }, + { + name: "Policy allows select but query requires also insert", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"select"})}, + }, + { + name: "Policy allows all actions", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"all"})}, + }, + { + name: "Policy many actions and many are required", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"delete", "insert", "select", "update"})}, + }, + { + name: "Policy exclude user from the select action", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup([]string{"select"})}, + }, + { + name: "Policy exclude user from the insert action, but query is select and insert", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup([]string{"insert"})}, + }, + { + name: "Policy exclude user from the insert action, but query is select ", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup([]string{"insert"})}, + }, + } + + runTests(t, tests) +} + +func TestDenyPermissionsForGroups(t *testing.T) { + tests := []testCase{} + + runTests(t, tests) +} + +func getDefaultGroupAllowPolicy(accessType []string) *ranger.Policy { + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, at := range accessType { + accesses = append(accesses, ranger.Access{Type: at}) + } + return accesses + }(), + }, + }, + } +} + +func getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup(excludeAccess []string) *ranger.Policy { + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + AllowExceptions: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, ex := range excludeAccess { + accesses = append(accesses, ranger.Access{Type: ex}) + } + return accesses + }(), + }, + }, + } +} diff --git a/pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go b/pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go deleted file mode 100644 index 9a88db0..0000000 --- a/pkg/rbac/ranger/tests/ranger_deny_user_policy_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package tests - -import ( - "testing" - - "github.com/patterninc/heimdall/pkg/rbac/ranger" -) - -func TestDenyPermissionsForUser(t *testing.T) { - tests := []testCase{ - { - name: "Policy denies select action for user", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyForUser([]string{"select"}), - }, - { - name: "Policy denies insert action for user", - query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyForUser([]string{"insert"}), - }, - { - name: "Policy denies update action for user but query is select", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyForUser([]string{"update"}), - }, - { - name: "Policy denies select and insert actions for user", - query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"select", "insert"}, []string{"all"}), - }, - { - name: "Policy denies all actions for user but exception allows select", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"all"}, []string{"select"}), - }, - } - - runTests(t, tests) -} - -func getAllowAllPolicyWithDenyForUser(denyAccess []string) []*ranger.Policy { - return []*ranger.Policy{ - { - ID: 1, - GUID: "policy-1", - IsEnabled: true, - Name: "Allow select for alice", - PolicyType: 0, - PolicyPriority: 1, - Resources: &ranger.Resource{ - Catalog: &ranger.ResourceField{ - Values: []string{"default_catalog"}, - IsExcludes: false, - }, - Schema: &ranger.ResourceField{ - Values: []string{"public"}, - IsExcludes: false, - }, - Table: &ranger.ResourceField{ - Values: []string{"table1"}, - IsExcludes: false, - }, - }, - PolicyItems: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: []ranger.Access{ - {Type: "all"}, - }, - }, - }, - DenyPolicyItems: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: func() []ranger.Access { - var accesses []ranger.Access - for _, a := range denyAccess { - accesses = append(accesses, ranger.Access{Type: a}) - } - return accesses - }(), - }, - }, - }, - } -} - -func getAllowAllPolicyWithDenyAndExceptionForUser(denyAccess, exceptionAccess []string) []*ranger.Policy { - return []*ranger.Policy{ - { - ID: 1, - GUID: "policy-1", - IsEnabled: true, - Name: "Allow select for alice", - PolicyType: 0, - PolicyPriority: 1, - Resources: &ranger.Resource{ - Catalog: &ranger.ResourceField{ - Values: []string{"default_catalog"}, - IsExcludes: false, - }, - Schema: &ranger.ResourceField{ - Values: []string{"public"}, - IsExcludes: false, - }, - Table: &ranger.ResourceField{ - Values: []string{"table1"}, - IsExcludes: false, - }, - }, - PolicyItems: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: []ranger.Access{ - {Type: "all"}, - }, - }, - }, - DenyPolicyItems: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: func() []ranger.Access { - var accesses []ranger.Access - for _, a := range denyAccess { - accesses = append(accesses, ranger.Access{Type: a}) - } - return accesses - }(), - }, - }, - DenyExceptions: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: func() []ranger.Access { - var accesses []ranger.Access - for _, a := range exceptionAccess { - accesses = append(accesses, ranger.Access{Type: a}) - } - return accesses - }(), - }, - }, - }, - } -} diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index 7a4a4e4..d9926b0 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -780,136 +780,6 @@ func TestResourcesSelection(t *testing.T) { } -func TestAllowPermissionsForUser(t *testing.T) { - tests := []testCase{ - { - name: "Policy allows all actions for user", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, - }, - { - name: "Policy allows select action for user", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, - }, - { - name: "Policy allows insert action for user, but query is select", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert"})}, - }, - { - name: "Policy allows multiple actions including select for user", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "select", "update"})}, - }, - { - name: "Policy allows multiple actions excluding select for user", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "update", "delete"})}, - }, - { - name: "No policy for user", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{}, - }, - { - name: "Policy allows select but query requires also insert", - query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, - }, - { - name: "Policy allows all actions", - query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, - }, - { - name: "Policy many actions and many are required", - query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"delete", "insert", "select", "update"})}, - }, - { - name: "Policy exclude user from the select action", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"select"})}, - }, - { - name: "Policy exclude user from the insert action, but query is select and insert", - query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: false, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, - }, - { - name: "Policy exclude user from the insert action, but query is select ", - query: "SELECT * FROM default_catalog.public.table1", - username: testUserName, - expectedResult: true, - users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, - }, - } - - runTests(t, tests) - -} - - - -func TestAllowPermissionsForGroups(t *testing.T) { - tests := []testCase{} - - runTests(t, tests) -} - -func TestDenyPermissionsForGroups(t *testing.T) { - tests := []testCase{} - - runTests(t, tests) -} - func runTests(t *testing.T, tests []testCase) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1025,92 +895,11 @@ func getAllowAllPolicy(resource *ranger.Resource, additionalResource *ranger.Res } } -func getDefaultUserAllowPolicy(accessType []string) *ranger.Policy { - return &ranger.Policy{ - ID: 1, - GUID: "policy-1", - IsEnabled: true, - Name: "Allow select for alice", - PolicyType: 0, - PolicyPriority: 1, - Resources: &ranger.Resource{ - Catalog: &ranger.ResourceField{ - Values: []string{"default_catalog"}, - IsExcludes: false, - }, - Schema: &ranger.ResourceField{ - Values: []string{"public"}, - IsExcludes: false, - }, - Table: &ranger.ResourceField{ - Values: []string{"table1"}, - IsExcludes: false, - }, - }, - PolicyItems: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: func() []ranger.Access { - var accesses []ranger.Access - for _, at := range accessType { - accesses = append(accesses, ranger.Access{Type: at}) - } - return accesses - }(), - }, - }, - } -} - -func getDefaultAllActionsUserPolicyWithExcludeForDefaultUser(excludeAccess []string) *ranger.Policy { - return &ranger.Policy{ - ID: 1, - GUID: "policy-1", - IsEnabled: true, - Name: "Allow select for alice", - PolicyType: 0, - PolicyPriority: 1, - Resources: &ranger.Resource{ - Catalog: &ranger.ResourceField{ - Values: []string{"default_catalog"}, - IsExcludes: false, - }, - Schema: &ranger.ResourceField{ - Values: []string{"public"}, - IsExcludes: false, - }, - Table: &ranger.ResourceField{ - Values: []string{"table1"}, - IsExcludes: false, - }, - }, - PolicyItems: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: []ranger.Access{ - {Type: "all"}, - }, - }, - }, - AllowExceptions: []ranger.PolicyItem{ - { - Users: []string{testUserName}, - Accesses: func() []ranger.Access { - var accesses []ranger.Access - for _, ex := range excludeAccess { - accesses = append(accesses, ranger.Access{Type: ex}) - } - return accesses - }(), - }, - }, - } -} -func getMockRangerClient(users map[string]*ranger.User, groups map[string]*ranger.Group, policies []*ranger.Policy) *mocks.Client { +func getMockRangerClient(users map[string]*ranger.User, groups map[string]*ranger.Group, policies []*ranger.Policy) ranger.ClientWrapper { m := new(mocks.Client) m.On("GetUsers").Return(users, nil) m.On("GetGroups").Return(groups, nil) m.On("GetPolicies", serviceName).Return(policies, nil) - return m + return ranger.ClientWrapper{Client: m} } diff --git a/pkg/rbac/ranger/tests/user_policy_test.go b/pkg/rbac/ranger/tests/user_policy_test.go new file mode 100644 index 0000000..d256751 --- /dev/null +++ b/pkg/rbac/ranger/tests/user_policy_test.go @@ -0,0 +1,363 @@ +package tests + +import ( + "testing" + + "github.com/patterninc/heimdall/pkg/rbac/ranger" +) + +func TestDenyPermissionsForUser(t *testing.T) { + tests := []testCase{ + { + name: "Policy denies select action for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForUser([]string{"select"}), + }, + { + name: "Policy denies insert action for user", + query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForUser([]string{"insert"}), + }, + { + name: "Policy denies update action for user but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForUser([]string{"update"}), + }, + { + name: "Policy denies select and insert actions for user", + query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"select", "insert"}, []string{"all"}), + }, + { + name: "Policy denies all actions for user but exception allows select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"all"}, []string{"select"}), + }, + } + + runTests(t, tests) +} + +func TestAllowPermissionsForUser(t *testing.T) { + tests := []testCase{ + { + name: "Policy allows all actions for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, + }, + { + name: "Policy allows select action for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, + }, + { + name: "Policy allows insert action for user, but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert"})}, + }, + { + name: "Policy allows multiple actions including select for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "select", "update"})}, + }, + { + name: "Policy allows multiple actions excluding select for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "update", "delete"})}, + }, + { + name: "No policy for user", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{}, + }, + { + name: "Policy allows select but query requires also insert", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, + }, + { + name: "Policy allows all actions", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, + }, + { + name: "Policy many actions and many are required", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"delete", "insert", "select", "update"})}, + }, + { + name: "Policy exclude user from the select action", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"select"})}, + }, + { + name: "Policy exclude user from the insert action, but query is select and insert", + query: "INSERT INTO default_catalog.public.table1 as SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, + }, + { + name: "Policy exclude user from the insert action, but query is select ", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, + }, + } + + runTests(t, tests) + +} + +func getAllowAllPolicyWithDenyForUser(denyAccess []string) []*ranger.Policy { + return []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range denyAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + }, + } +} + +func getAllowAllPolicyWithDenyAndExceptionForUser(denyAccess, exceptionAccess []string) []*ranger.Policy { + return []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range denyAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + DenyExceptions: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range exceptionAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + }, + } +} + +func getDefaultUserAllowPolicy(accessType []string) *ranger.Policy { + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, at := range accessType { + accesses = append(accesses, ranger.Access{Type: at}) + } + return accesses + }(), + }, + }, + } +} + +func getDefaultAllActionsUserPolicyWithExcludeForDefaultUser(excludeAccess []string) *ranger.Policy { + return &ranger.Policy{ + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + AllowExceptions: []ranger.PolicyItem{ + { + Users: []string{testUserName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, ex := range excludeAccess { + accesses = append(accesses, ranger.Access{Type: ex}) + } + return accesses + }(), + }, + }, + } +} From 2ff5b5d8299d67fbebcdf3e7c01bcf60e8f0eba9 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 11 Sep 2025 10:08:24 -0600 Subject: [PATCH 08/14] Add more tests --- pkg/rbac/ranger/tests/group_policy_test.go | 199 ++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/pkg/rbac/ranger/tests/group_policy_test.go b/pkg/rbac/ranger/tests/group_policy_test.go index c841d54..cdcc12a 100644 --- a/pkg/rbac/ranger/tests/group_policy_test.go +++ b/pkg/rbac/ranger/tests/group_policy_test.go @@ -122,7 +122,98 @@ func TestAllowPermissionsForGroups(t *testing.T) { } func TestDenyPermissionsForGroups(t *testing.T) { - tests := []testCase{} + tests := []testCase{ + { + name: "Policy denies select action for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForGroup([]string{"select"}), + }, + { + name: "Policy denies insert action for group, but query is select", + query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForGroup([]string{"insert"}), + }, + { + name: "Policy denies update action for group but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForGroup([]string{"update"}), + }, + { + name: "Policy denies multiple actions including select for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForGroup([]string{"insert", "select", "update"}), + }, + { + name: "Policy denies multiple actions excluding select for group", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyForGroup([]string{"insert", "update", "delete"}), + }, + { + name: "Policy denies select and insert actions for group", + query: "INSERT INTO default_catalog.public.table1 VALUES (1, 'data')", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"select", "insert"}, []string{"all"}), + }, + { + name: "Policy denies all actions for group but exception allows select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"select"}), + }, + { + name: "Policy denies all actions for group but exception allows insert, but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: false, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"insert"}), + }, + { + name: "Policy denies all actions for group but exception allows select and insert, but query is select", + query: "SELECT * FROM default_catalog.public.table1", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"select", "insert"}), + }, + { + name: "Policy denies all actions for group but exception allows select and insert, but query is insert", + query: "INSERT INTO default_catalog.public.table1 VALUES (SELECT * FROM default_catalog.public.table1)", + username: testUserName, + expectedResult: true, + users: testDefaultUsers, + groups: testDefaultGroups, + policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"insert", "select"}), + }, + } runTests(t, tests) } @@ -208,3 +299,109 @@ func getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup(excludeAccess []s }, } } + +func getAllowAllPolicyWithDenyForGroup(denyAccess []string) []*ranger.Policy { + return []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range denyAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + }, + } +} + +func getAllowAllPolicyWithDenyAndExceptionForGroup(denyAccess, exceptionAccess []string) []*ranger.Policy { + return []*ranger.Policy{ + { + ID: 1, + GUID: "policy-1", + IsEnabled: true, + Name: "Allow select for alice", + PolicyType: 0, + PolicyPriority: 1, + Resources: &ranger.Resource{ + Catalog: &ranger.ResourceField{ + Values: []string{"default_catalog"}, + IsExcludes: false, + }, + Schema: &ranger.ResourceField{ + Values: []string{"public"}, + IsExcludes: false, + }, + Table: &ranger.ResourceField{ + Values: []string{"table1"}, + IsExcludes: false, + }, + }, + PolicyItems: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: []ranger.Access{ + {Type: "all"}, + }, + }, + }, + DenyPolicyItems: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range denyAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + DenyExceptions: []ranger.PolicyItem{ + { + Groups: []string{testGroupName}, + Accesses: func() []ranger.Access { + var accesses []ranger.Access + for _, a := range exceptionAccess { + accesses = append(accesses, ranger.Access{Type: a}) + } + return accesses + }(), + }, + }, + }, + } +} From f70696768363cae976dce00c4a83d8f90d471981 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 11 Sep 2025 13:56:16 -0600 Subject: [PATCH 09/14] Add rbacs on cluster level --- cmd/heimdall/heimdall.go | 2 +- configs/local.yaml | 36 +--- docker-compose.yaml | 2 +- internal/pkg/heimdall/heimdall.go | 6 +- internal/pkg/object/command/trino/trino.go | 5 +- pkg/rbac/README.md | 228 +++++++++++++++++++++ pkg/rbac/ranger/client.go | 2 +- 7 files changed, 244 insertions(+), 37 deletions(-) create mode 100644 pkg/rbac/README.md diff --git a/cmd/heimdall/heimdall.go b/cmd/heimdall/heimdall.go index 067026a..08bef3b 100644 --- a/cmd/heimdall/heimdall.go +++ b/cmd/heimdall/heimdall.go @@ -29,7 +29,7 @@ var ( func init() { - flag.StringVar(&configFile, `conf`, `/Users/ivanhladush/git/heimdall/configs/local.yaml`, `config file`) + flag.StringVar(&configFile, `conf`, `/etc/heimdall/heimdall.yaml`, `config file`) flag.Parse() } diff --git a/configs/local.yaml b/configs/local.yaml index 72d26e9..81d2e3f 100644 --- a/configs/local.yaml +++ b/configs/local.yaml @@ -8,13 +8,13 @@ pool: sleep: 500 # # plugins location -# plugin_directory: ./plugins +plugin_directory: ./plugins -# # auth plugin -# auth: -# plugin: ./plugins/auth_header.so -# context: -# header: X-Heimdall-User +# auth plugin +auth: + plugin: ./plugins/auth_header.so + context: + header: X-Heimdall-User # supported commands commands: @@ -42,27 +42,3 @@ clusters: - type:localhost - data:local -rbacs: - - name: trino - type: apache_ranger - service_name: TrinoRanger - sync_interval_in_minutes: 1 - client: - url: http://localhost:6080 - username: admin - password: admin - parser: - type: trino - default_catalog: hive - - - name: trino2 - type: apache_ranger - service_name: TrinoRanger - sync_interval_in_minutes: 1 - client: - url: http://localhost:6080 - username: admin - password: - parser: - type: trino - default_catalog: hive diff --git a/docker-compose.yaml b/docker-compose.yaml index 15d3151..f7ad0d1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ --- services: postgres: - image: postgres:16 + image: postgres:16.1 container_name: heimdall_postgres environment: POSTGRES_USER: heimdall diff --git a/internal/pkg/heimdall/heimdall.go b/internal/pkg/heimdall/heimdall.go index e655ba4..be9ba42 100644 --- a/internal/pkg/heimdall/heimdall.go +++ b/internal/pkg/heimdall/heimdall.go @@ -133,9 +133,9 @@ func (h *Heimdall) Init() error { } // // let's record command in the database - // if err := h.clusterUpsert(c); err != nil { - // return err - // } + if err := h.clusterUpsert(c); err != nil { + return err + } if len(c.RBACNames) > 0 { for _, rbacName := range c.RBACNames { r, found := rbacsByName[rbacName] diff --git a/internal/pkg/object/command/trino/trino.go b/internal/pkg/object/command/trino/trino.go index 79dc3c8..78e01ec 100644 --- a/internal/pkg/object/command/trino/trino.go +++ b/internal/pkg/object/command/trino/trino.go @@ -88,15 +88,17 @@ func (t *commandContext) handler(r *plugin.Runtime, j *job.Job, c *cluster.Clust } func canQueryBeExecuted(query, user string, c *cluster.Cluster) bool { + fmt.Printf("checking if user %s can run the query: %s\n", user, query) // todo add metrics for time spent here if query == `` { return false } for _, rbac := range c.RBACs { + println("checking rbac", rbac.GetName()) allowed, err := rbac.HasAccess(user, query) if err != nil { - log.Printf("failed to check rbac: %w", err) + log.Printf("failed to check rbac: %v", err) return false } if !allowed { @@ -104,5 +106,6 @@ func canQueryBeExecuted(query, user string, c *cluster.Cluster) bool { return false } } + log.Printf("user %s is allowed to run the query", user) return true } diff --git a/pkg/rbac/README.md b/pkg/rbac/README.md new file mode 100644 index 0000000..ca76300 --- /dev/null +++ b/pkg/rbac/README.md @@ -0,0 +1,228 @@ +# RBAC Module + +The RBAC (Role-Based Access Control) module provides authentication and authorization capabilities for Heimdall, specifically designed to integrate with Apache Ranger for fine-grained access control over SQL resources. + +## Overview + +This module enables Heimdall to: +- Authenticate users against Apache Ranger +- Authorize SQL queries based on Ranger policies +- Support table-level, schema-level, and catalog-level access control +- Handle user groups and permissions +- Automatically sync policies from Ranger + +## Architecture + +The module consists of several key components: + +### Core Interfaces + +- **`RBAC`**: Main interface for access control providers +- **`Client`**: Interface for communicating with external systems (Apache Ranger) + +### Apache Ranger Integration + +The module currently supports Apache Ranger as the primary RBAC provider through: + +- **`ApacheRanger`**: Main implementation of RBAC interface +- **`client`**: HTTP client for Ranger API communication +- **`Policy`**: Represents Ranger policies with resources and permissions +- **`User`** and **`Group`**: Represent Ranger users and groups + +## Configuration + +### YAML Configuration Example + +```yaml +rbac: + - type: apache_ranger + name: my-ranger + service_name: my_service + sync_interval_in_minutes: 30 + client: + url: https://ranger.example.com + username: admin + password: secret + parser: + type: trino + default_catalog: hive +``` +### YAML Cluster Configuration + +To enable RBAC in your cluster configuration, add the `rbacs` section and specify the RBAC provider names you want to use. This allows you to define a chain of permission providers. + +Example: + +```yaml +cluster: + name: my-cluster + rbacs: + - my-ranger + - another-rbac-provider +``` + +The `rbacs` list enables multiple RBAC providers to be evaluated in order, allowing flexible and layered access control. + +### Configuration Parameters + +- **`type`**: RBAC provider type (`apache_ranger`) +- **`name`**: Unique identifier for this RBAC instance +- **`service_name`**: Ranger service name to fetch policies from +- **`sync_interval_in_minutes`**: How often to sync policies from Ranger +- **`client`**: Ranger connection configuration +- **`parser`**: SQL parser configuration for query analysis + +## Usage + +### Initialization + +```go +import "github.com/patterninc/heimdall/pkg/rbac" + +// Parse configuration +var rbacs rbac.RBACs +err := yaml.Unmarshal(configData, &rbacs) + +// Initialize RBAC providers +ctx := context.Background() +for _, rbac := range rbacs { + err := rbac.Init(ctx) + if err != nil { + log.Fatal(err) + } +} +``` + +### Access Control + +```go +// Check if user has access to execute a query +user := "john.doe" +query := "SELECT * FROM catalog.schema.table" + +hasAccess, err := rbac.HasAccess(user, query) +if err != nil { + log.Error("Error checking access:", err) + return +} + +if !hasAccess { + log.Info("Access denied for user:", user) + return +} + +// Execute query... +``` + +## Features + +### Supported SQL Actions + +The module supports fine-grained control over SQL operations: + +- `SELECT`: Read data from tables +- `INSERT`: Insert data into tables +- `UPDATE`: Update existing data +- `DELETE`: Delete data from tables +- `CREATE`: Create new objects (tables, schemas, etc.) +- `DROP`: Drop existing objects +- `ALTER`: Modify existing objects +- `USE`: Use/switch to a schema or catalog +- `GRANT`/`REVOKE`: Manage permissions +- `SHOW`: Show system information +- `IMPERSONATE`: Act as another user +- `EXECUTE`: Execute procedures/functions + +### Resource Matching + +Policies support wildcard patterns for flexible resource matching: + +- `*`: Matches any characters +- `?`: Matches single character +- Regular expressions for complex patterns + +### Policy Types + +- **Allow Policies**: Grant specific permissions to users/groups +- **Deny Policies**: Explicitly deny permissions (takes precedence) +- **Exceptions**: Override allow/deny policies for specific cases + +### Automatic Synchronization + +- Policies are automatically synced from Ranger at configured intervals +- Users and groups are kept up-to-date +- Background goroutine handles sync without blocking operations + +## API Reference + +### RBAC Interface + +```go +type RBAC interface { + Init(ctx context.Context) error + HasAccess(user string, query string) (bool, error) + GetName() string +} +``` + +### Client Interface + +```go +type Client interface { + GetUsers() (map[string]*User, error) + GetGroups() (map[string]*Group, error) + GetPolicies(serviceName string) ([]*Policy, error) +} +``` + +## Error Handling + +The module handles various error scenarios: + +- **Network failures**: Retries and graceful degradation +- **Invalid policies**: Logs warnings and skips malformed policies +- **Unknown users**: Returns access denied for unknown users +- **Parsing errors**: Returns detailed error messages for invalid queries + +## Performance Considerations + +- **Caching**: User permissions are cached in memory for fast access +- **Batch requests**: API calls use pagination for large datasets +- **Background sync**: Policy updates don't block query processing +- **Regex compilation**: Resource patterns are pre-compiled for efficiency + +## Security + +- **Basic Authentication**: Secure communication with Ranger +- **Case-insensitive usernames**: Consistent user matching +- **Deny-by-default**: Unknown users/resources are denied access +- **Audit logging**: All access decisions are logged + +## Troubleshooting + +### Common Issues + +1. **User not found in policies** + - Ensure user exists in Ranger + - Check group memberships + - Verify sync is working + +2. **Access denied unexpectedly** + - Check policy resource patterns + - Verify deny policies aren't blocking access + - Review query parsing results + +3. **Sync failures** + - Verify Ranger connectivity + - Check authentication credentials + - Review Ranger service configuration + +### Debugging + +Enable verbose logging to see detailed access control decisions: + +```go +log.SetLevel(log.DebugLevel) +``` + +This will show policy matches, resource patterns, and access decisions for each query. \ No newline at end of file diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go index 791e62a..ce0f874 100644 --- a/pkg/rbac/ranger/client.go +++ b/pkg/rbac/ranger/client.go @@ -201,7 +201,7 @@ func (c *client) executeRequest(method string, endpoint string, v interface{}, r // executeBatchRequest performs paginated API requests and returns all aggregated results func (c *client) executeBatchRequest(method string, endpoint string) ([]getResponse, error) { results := make([]getResponse, 500) - pageSize := 200 + pageSize := 500 startIndex := 0 for { From a553838d71f06996b2a32182723649d6bacd67bf Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 11 Sep 2025 14:00:30 -0600 Subject: [PATCH 10/14] Small fixes --- configs/local.yaml | 9 +++------ internal/pkg/heimdall/heimdall.go | 2 +- internal/pkg/object/command/trino/trino.go | 3 +-- pkg/rbac/README.md | 20 ++++++++++---------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/configs/local.yaml b/configs/local.yaml index 81d2e3f..b420f30 100644 --- a/configs/local.yaml +++ b/configs/local.yaml @@ -1,3 +1,4 @@ + # database settings database: connection_string: "postgres://heimdall:heimdall@postgres:5432/heimdall?sslmode=disable" @@ -7,7 +8,7 @@ pool: size: 5 sleep: 500 -# # plugins location +# plugins location plugin_directory: ./plugins # auth plugin @@ -35,10 +36,6 @@ clusters: status: active version: 0.0.1 description: Just a localhost - rbacs: - - trino - - trino2 tags: - type:localhost - - data:local - + - data:local \ No newline at end of file diff --git a/internal/pkg/heimdall/heimdall.go b/internal/pkg/heimdall/heimdall.go index be9ba42..dddd274 100644 --- a/internal/pkg/heimdall/heimdall.go +++ b/internal/pkg/heimdall/heimdall.go @@ -132,7 +132,7 @@ func (h *Heimdall) Init() error { return err } - // // let's record command in the database + // let's record command in the database if err := h.clusterUpsert(c); err != nil { return err } diff --git a/internal/pkg/object/command/trino/trino.go b/internal/pkg/object/command/trino/trino.go index 78e01ec..d18ae99 100644 --- a/internal/pkg/object/command/trino/trino.go +++ b/internal/pkg/object/command/trino/trino.go @@ -88,14 +88,13 @@ func (t *commandContext) handler(r *plugin.Runtime, j *job.Job, c *cluster.Clust } func canQueryBeExecuted(query, user string, c *cluster.Cluster) bool { - fmt.Printf("checking if user %s can run the query: %s\n", user, query) + log.Printf("checking if user %s can run the query: %s\n", user, query) // todo add metrics for time spent here if query == `` { return false } for _, rbac := range c.RBACs { - println("checking rbac", rbac.GetName()) allowed, err := rbac.HasAccess(user, query) if err != nil { log.Printf("failed to check rbac: %v", err) diff --git a/pkg/rbac/README.md b/pkg/rbac/README.md index ca76300..4666f6e 100644 --- a/pkg/rbac/README.md +++ b/pkg/rbac/README.md @@ -1,11 +1,10 @@ # RBAC Module -The RBAC (Role-Based Access Control) module provides authentication and authorization capabilities for Heimdall, specifically designed to integrate with Apache Ranger for fine-grained access control over SQL resources. +The RBAC (Role-Based Access Control) module provides authorization capabilities for Heimdall, specifically designed to integrate with Apache Ranger for fine-grained access control over SQL resources. ## Overview This module enables Heimdall to: -- Authenticate users against Apache Ranger - Authorize SQL queries based on Ranger policies - Support table-level, schema-level, and catalog-level access control - Handle user groups and permissions @@ -47,6 +46,15 @@ rbac: type: trino default_catalog: hive ``` +### Configuration Parameters + +- **`type`**: RBAC provider type (`apache_ranger`) +- **`name`**: Unique identifier for this RBAC instance +- **`service_name`**: Ranger service name to fetch policies from +- **`sync_interval_in_minutes`**: How often to sync policies from Ranger +- **`client`**: Ranger connection configuration +- **`parser`**: SQL parser configuration for query analysis + ### YAML Cluster Configuration To enable RBAC in your cluster configuration, add the `rbacs` section and specify the RBAC provider names you want to use. This allows you to define a chain of permission providers. @@ -63,14 +71,6 @@ cluster: The `rbacs` list enables multiple RBAC providers to be evaluated in order, allowing flexible and layered access control. -### Configuration Parameters - -- **`type`**: RBAC provider type (`apache_ranger`) -- **`name`**: Unique identifier for this RBAC instance -- **`service_name`**: Ranger service name to fetch policies from -- **`sync_interval_in_minutes`**: How often to sync policies from Ranger -- **`client`**: Ranger connection configuration -- **`parser`**: SQL parser configuration for query analysis ## Usage From 9233232c4ff54ac31299011b168f2fb1396a392b Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Mon, 15 Sep 2025 09:08:15 -0600 Subject: [PATCH 11/14] Fix spelling --- pkg/rbac/ranger/ranger.go | 38 +++++++++---------- .../ranger/tests/ranger_policy_check_test.go | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go index 8771023..5463d24 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/pkg/rbac/ranger/ranger.go @@ -15,7 +15,7 @@ type ApacheRanger struct { Client ClientWrapper `yaml:"client,omitempty" json:"client,omitempty"` SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` AccessReceiver parser.AccessReceiver - permitionsByUser map[string]*UserPermitions + permissionsByUser map[string]*UserPermissions Parser ParserConfig `yaml:"parser,omitempty" json:"parser,omitempty"` } @@ -24,15 +24,15 @@ type ParserConfig struct { DefaultCatalog string `yaml:"default_catalog,omitempty" json:"default_catalog,omitempty"` } -type PermitionStatus int +type PermissionStatus int const ( - PermitionStatusAllow PermitionStatus = iota - PermitionStatusDeny - PermitionStatusUnknown + PermissionStatusAllow PermissionStatus = iota + PermissionStatusDeny + PermissionStatusUnknown ) -type UserPermitions struct { +type UserPermissions struct { AllowPolicys map[parser.Action][]*Policy DenyPolicys map[parser.Action][]*Policy } @@ -48,8 +48,8 @@ func (ar *ApacheRanger) Init(ctx context.Context) error { func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { user = strings.ToLower(user) - if _, ok := ar.permitionsByUser[user]; !ok { - log.Println("User not found in ranger policies", "user", user) + if _, ok := ar.permissionsByUser[user]; !ok { + log.Println("User not found in ranger policies. User: ", user) return false, nil } accessList, err := ar.AccessReceiver.ParseAccess(query) @@ -57,17 +57,17 @@ func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { return false, err } - permitions := ar.permitionsByUser[user] + permissions := ar.permissionsByUser[user] for _, access := range accessList { - for _, permition := range permitions.DenyPolicys[access.Action()] { + for _, permition := range permissions.DenyPolicys[access.Action()] { if permition.doesControlAnAccess(access) { log.Println("Access denied by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) return false, nil } } foundAllowPolicy := false - for _, permition := range permitions.AllowPolicys[access.Action()] { + for _, permition := range permissions.AllowPolicys[access.Action()] { if permition.doesControlAnAccess(access) { log.Println("Access allowed by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) foundAllowPolicy = true @@ -116,7 +116,7 @@ func (r *ApacheRanger) SyncState() error { } } - newPermitionsByUser := map[string]*UserPermitions{} + newPermissionsByUser := map[string]*UserPermissions{} for _, policy := range policies { if !policy.IsEnabled { continue @@ -133,30 +133,30 @@ func (r *ApacheRanger) SyncState() error { controlledActions := policy.getControlledActions(usersByGroup) for userName, actions := range controlledActions.allowedActionsByUser { - if _, ok := newPermitionsByUser[userName]; !ok { - newPermitionsByUser[userName] = &UserPermitions{ + if _, ok := newPermissionsByUser[userName]; !ok { + newPermissionsByUser[userName] = &UserPermissions{ AllowPolicys: map[parser.Action][]*Policy{}, DenyPolicys: map[parser.Action][]*Policy{}, } } for _, action := range actions { - newPermitionsByUser[userName].AllowPolicys[action] = append(newPermitionsByUser[userName].AllowPolicys[action], policy) + newPermissionsByUser[userName].AllowPolicys[action] = append(newPermissionsByUser[userName].AllowPolicys[action], policy) } } for userName, actions := range controlledActions.deniedActionsByUser { - if _, ok := newPermitionsByUser[userName]; !ok { - newPermitionsByUser[userName] = &UserPermitions{ + if _, ok := newPermissionsByUser[userName]; !ok { + newPermissionsByUser[userName] = &UserPermissions{ AllowPolicys: map[parser.Action][]*Policy{}, DenyPolicys: map[parser.Action][]*Policy{}, } } for _, action := range actions { - newPermitionsByUser[userName].DenyPolicys[action] = append(newPermitionsByUser[userName].DenyPolicys[action], policy) + newPermissionsByUser[userName].DenyPolicys[action] = append(newPermissionsByUser[userName].DenyPolicys[action], policy) } } } - r.permitionsByUser = newPermitionsByUser + r.permissionsByUser = newPermissionsByUser log.Println("Syncing users and groups from Apache Ranger for service:", r.ServiceName) return nil } diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index d9926b0..1a63f79 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -610,7 +610,7 @@ func TestRangerPolicyCheck(t *testing.T) { } // TestResourcesSelection tests the resource selection logic in Ranger policies. -// In this tests users always have all permitions +// In this tests users always have all permissions func TestResourcesSelection(t *testing.T) { tests := []testCase{ { From fa18911268c59e0b3416a7e418809f2f8e23fab7 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Tue, 16 Sep 2025 10:00:52 -0600 Subject: [PATCH 12/14] First refactoring after review --- pkg/rbac/ranger/client.go | 61 ++++----- pkg/rbac/ranger/mocks/Client.go | 30 ----- pkg/rbac/ranger/ranger.go | 124 +++++++----------- pkg/rbac/ranger/tests/group_policy_test.go | 22 ---- .../ranger/tests/ranger_policy_check_test.go | 124 +++++------------- pkg/rbac/ranger/tests/user_policy_test.go | 35 ++--- pkg/rbac/rbac.go | 45 ++++--- 7 files changed, 144 insertions(+), 297 deletions(-) diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go index ce0f874..900303e 100644 --- a/pkg/rbac/ranger/client.go +++ b/pkg/rbac/ranger/client.go @@ -16,13 +16,11 @@ import ( const ( getUsersEndpoint = `/service/xusers/users` getServicePoliciesEndpoint = `/service/public/v2/api/service/%s/policy` - getGroupsEndpoint = `/service/xusers/groups` ) //go:generate go run github.com/vektra/mockery/v2@v2.53.4 --name=Client --output=./mocks --outpkg=mocks type Client interface { GetUsers() (map[string]*User, error) - GetGroups() (map[string]*Group, error) GetPolicies(serviceName string) ([]*Policy, error) } @@ -44,24 +42,21 @@ func (cw *ClientWrapper) GetUsers() (map[string]*User, error) { return cw.Client.GetUsers() } -func (cw *ClientWrapper) GetGroups() (map[string]*Group, error) { - return cw.Client.GetGroups() -} func (cw *ClientWrapper) GetPolicies(serviceName string) ([]*Policy, error) { return cw.Client.GetPolicies(serviceName) } type User struct { - ID int64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - EmailAddress string `json:"emailAddress,omitempty"` - UserRoleList []string `json:"userRoleList,omitempty"` - Password string `json:"password,omitempty"` - SyncSource string `json:"syncSource,omitempty"` - GroupIdList []int64 `json:"groupIdList,omitempty"` + ID int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + EmailAddress string `json:"emailAddress,omitempty"` + UserRoleList []string `json:"userRoleList,omitempty"` + Password string `json:"password,omitempty"` + SyncSource string `json:"syncSource,omitempty"` + GroupNameList []string `json:"groupNameList,omitempty"` } type Group struct { @@ -72,11 +67,18 @@ type Group struct { } type getResponse struct { - PageSize int `json:"pageSize"` - StartIndex int `json:"startIndex"` - ResultSize int `json:"resultSize"` - VXUsers []*User `json:"vXUsers,omitempty"` - VXGroups []*Group `json:"vXGroups,omitempty"` + PageSize int `json:"pageSize"` + StartIndex int `json:"startIndex"` + ResultSize int `json:"resultSize"` + VXUsers []*User `json:"vXUsers,omitempty"` + VXGroups []*Group `json:"vXGroups,omitempty"` + VXGroupUsers []*vXGroupUsers `json:"vXGroupUsers,omitempty"` +} + +type vXGroupUsers struct { + ID int64 `json:"id,omitempty"` + Name string `json:"name,omitempty"` + FirstName string `json:"firstName,omitempty"` } type client struct { @@ -118,24 +120,6 @@ func (c *client) GetUsers() (map[string]*User, error) { } -func (c *client) GetGroups() (map[string]*Group, error) { - responses, err := c.executeBatchRequest(http.MethodGet, getGroupsEndpoint) - if err != nil { - return nil, err - } - - groupsMap := make(map[string]*Group) - - for _, resp := range responses { - for _, group := range resp.VXGroups { - groupsMap[group.Name] = group - } - } - - log.Printf("Number of Ranger Groups pulled: %d\n", len(groupsMap)) - return groupsMap, nil -} - func (c *client) GetPolicies(serviceName string) ([]*Policy, error) { var policies []*Policy err := c.executeRequest(http.MethodGet, fmt.Sprintf(getServicePoliciesEndpoint, serviceName), &policies, nil) @@ -191,6 +175,9 @@ func (c *client) executeRequest(method string, endpoint string, v interface{}, r return fmt.Errorf("request to %s failed with status %s\n%s", req.URL.String(), resp.Status, bodyString) } + vals, _ := io.ReadAll(resp.Body) + fmt.Println(string(vals)) + resp.Body = io.NopCloser(bytes.NewReader(vals)) if v != nil { return json.NewDecoder(resp.Body).Decode(v) } diff --git a/pkg/rbac/ranger/mocks/Client.go b/pkg/rbac/ranger/mocks/Client.go index 9f5d423..5ce3ac6 100644 --- a/pkg/rbac/ranger/mocks/Client.go +++ b/pkg/rbac/ranger/mocks/Client.go @@ -12,36 +12,6 @@ type Client struct { mock.Mock } -// GetGroups provides a mock function with no fields -func (_m *Client) GetGroups() (map[string]*ranger.Group, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetGroups") - } - - var r0 map[string]*ranger.Group - var r1 error - if rf, ok := ret.Get(0).(func() (map[string]*ranger.Group, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() map[string]*ranger.Group); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]*ranger.Group) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetPolicies provides a mock function with given fields: serviceName func (_m *Client) GetPolicies(serviceName string) ([]*ranger.Policy, error) { ret := _m.Called(serviceName) diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go index 5463d24..3cc3fee 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/pkg/rbac/ranger/ranger.go @@ -9,14 +9,15 @@ import ( "github.com/patterninc/heimdall/pkg/sql/parser" ) -type ApacheRanger struct { - Name string `yaml:"name,omitempty" json:"name,omitempty"` - ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` - Client ClientWrapper `yaml:"client,omitempty" json:"client,omitempty"` - SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` - AccessReceiver parser.AccessReceiver - permissionsByUser map[string]*UserPermissions - Parser ParserConfig `yaml:"parser,omitempty" json:"parser,omitempty"` +// only private +// add links +type Ranger struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` + Client *ClientWrapper `yaml:"client,omitempty" json:"client,omitempty"` + SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` + AccessReceiver parser.AccessReceiver `yaml:"parser,omitempty" json:"parser,omitempty"` + permissionsByUser map[string]*UserPermissions } type ParserConfig struct { @@ -24,50 +25,61 @@ type ParserConfig struct { DefaultCatalog string `yaml:"default_catalog,omitempty" json:"default_catalog,omitempty"` } -type PermissionStatus int - -const ( - PermissionStatusAllow PermissionStatus = iota - PermissionStatusDeny - PermissionStatusUnknown -) - type UserPermissions struct { - AllowPolicys map[parser.Action][]*Policy - DenyPolicys map[parser.Action][]*Policy + AllowPolicies map[parser.Action][]*Policy // todo AllowPolicies + DenyPolicies map[parser.Action][]*Policy } -func (ar *ApacheRanger) Init(ctx context.Context) error { +func (r *Ranger) Init(ctx context.Context) error { // first time lets sync state explicitly - if err := ar.SyncState(); err != nil { + if err := r.SyncState(); err != nil { return err } - ar.startSyncPolicies(ctx) + go func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + ticker := time.NewTicker(time.Duration(r.SyncIntervalInMinutes) * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Println("Stopping Apache Ranger sync goroutine") + return + case <-ticker.C: + log.Println("Syncing policies from Apache Ranger for service:", r.ServiceName) + if err := r.SyncState(); err != nil { + log.Println("Error syncing users and groups from Apache Ranger", "error", err) + } + } + } + }() return nil } -func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { +func (r *Ranger) HasAccess(user string, query string) (bool, error) { user = strings.ToLower(user) - if _, ok := ar.permissionsByUser[user]; !ok { + if _, ok := r.permissionsByUser[user]; !ok { log.Println("User not found in ranger policies. User: ", user) return false, nil } - accessList, err := ar.AccessReceiver.ParseAccess(query) + accessList, err := r.AccessReceiver.ParseAccess(query) if err != nil { return false, err } - permissions := ar.permissionsByUser[user] + permissions := r.permissionsByUser[user] for _, access := range accessList { - for _, permition := range permissions.DenyPolicys[access.Action()] { + for _, permition := range permissions.DenyPolicies[access.Action()] { if permition.doesControlAnAccess(access) { log.Println("Access denied by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) return false, nil } } foundAllowPolicy := false - for _, permition := range permissions.AllowPolicys[access.Action()] { + for _, permition := range permissions.AllowPolicies[access.Action()] { if permition.doesControlAnAccess(access) { log.Println("Access allowed by ranger policy", "user", user, "query", query, "policy", permition.Name, "action", access.Action(), "resource", access.QualifiedName()) foundAllowPolicy = true @@ -82,11 +94,11 @@ func (ar *ApacheRanger) HasAccess(user string, query string) (bool, error) { return true, nil } -func (r *ApacheRanger) GetName() string { +func (r *Ranger) GetName() string { return r.Name } -func (r *ApacheRanger) SyncState() error { +func (r *Ranger) SyncState() error { policies, err := r.Client.GetPolicies(r.ServiceName) if err != nil { return err @@ -95,24 +107,11 @@ func (r *ApacheRanger) SyncState() error { if err != nil { return err } - groups, err := r.Client.GetGroups() - if err != nil { - return err - } - - log.Println("Users:", len(users), "Groups:", len(groups), "Policies:", len(policies)) - groupByID := map[int64]*Group{} usersByGroup := map[string][]string{} - for _, group := range groups { - groupByID[group.ID] = group - } - for _, user := range users { - for _, gid := range user.GroupIdList { - if group, ok := groupByID[gid]; ok { - usersByGroup[group.Name] = append(usersByGroup[group.Name], user.Name) - } + for _, gName := range user.GroupNameList { + usersByGroup[gName] = append(usersByGroup[gName], user.Name) } } @@ -130,28 +129,28 @@ func (r *ApacheRanger) SyncState() error { log.Println("Error initializing policy:", err) return err } - + controlledActions := policy.getControlledActions(usersByGroup) for userName, actions := range controlledActions.allowedActionsByUser { if _, ok := newPermissionsByUser[userName]; !ok { newPermissionsByUser[userName] = &UserPermissions{ - AllowPolicys: map[parser.Action][]*Policy{}, - DenyPolicys: map[parser.Action][]*Policy{}, + AllowPolicies: map[parser.Action][]*Policy{}, + DenyPolicies: map[parser.Action][]*Policy{}, } } for _, action := range actions { - newPermissionsByUser[userName].AllowPolicys[action] = append(newPermissionsByUser[userName].AllowPolicys[action], policy) + newPermissionsByUser[userName].AllowPolicies[action] = append(newPermissionsByUser[userName].AllowPolicies[action], policy) } } for userName, actions := range controlledActions.deniedActionsByUser { if _, ok := newPermissionsByUser[userName]; !ok { newPermissionsByUser[userName] = &UserPermissions{ - AllowPolicys: map[parser.Action][]*Policy{}, - DenyPolicys: map[parser.Action][]*Policy{}, + AllowPolicies: map[parser.Action][]*Policy{}, + DenyPolicies: map[parser.Action][]*Policy{}, } } for _, action := range actions { - newPermissionsByUser[userName].DenyPolicys[action] = append(newPermissionsByUser[userName].DenyPolicys[action], policy) + newPermissionsByUser[userName].DenyPolicies[action] = append(newPermissionsByUser[userName].DenyPolicies[action], policy) } } } @@ -160,26 +159,3 @@ func (r *ApacheRanger) SyncState() error { log.Println("Syncing users and groups from Apache Ranger for service:", r.ServiceName) return nil } - -func (ar *ApacheRanger) startSyncPolicies(ctx context.Context) { - go func() { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - ticker := time.NewTicker(time.Duration(ar.SyncIntervalInMinutes) * time.Minute) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - log.Println("Stopping Apache Ranger sync goroutine") - return - case <-ticker.C: - log.Println("Syncing policies from Apache Ranger for service:", ar.ServiceName) - if err := ar.SyncState(); err != nil { - log.Println("Error syncing users and groups from Apache Ranger", "error", err) - } - } - } - }() -} diff --git a/pkg/rbac/ranger/tests/group_policy_test.go b/pkg/rbac/ranger/tests/group_policy_test.go index cdcc12a..a816717 100644 --- a/pkg/rbac/ranger/tests/group_policy_test.go +++ b/pkg/rbac/ranger/tests/group_policy_test.go @@ -14,7 +14,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"all"})}, }, { @@ -23,7 +22,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"select"})}, }, { @@ -32,7 +30,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"insert"})}, }, { @@ -41,7 +38,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"insert", "select", "update"})}, }, { @@ -50,7 +46,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"insert", "update", "delete"})}, }, { @@ -59,7 +54,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{}, }, { @@ -68,7 +62,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"select"})}, }, { @@ -77,7 +70,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"all"})}, }, { @@ -86,7 +78,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultGroupAllowPolicy([]string{"delete", "insert", "select", "update"})}, }, { @@ -95,7 +86,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup([]string{"select"})}, }, { @@ -104,7 +94,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup([]string{"insert"})}, }, { @@ -113,7 +102,6 @@ func TestAllowPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultAllActionsGroupPolicyWithExcludeForDefaultGroup([]string{"insert"})}, }, } @@ -129,7 +117,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyForGroup([]string{"select"}), }, { @@ -138,7 +125,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyForGroup([]string{"insert"}), }, { @@ -147,7 +133,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyForGroup([]string{"update"}), }, { @@ -156,7 +141,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyForGroup([]string{"insert", "select", "update"}), }, { @@ -165,7 +149,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyForGroup([]string{"insert", "update", "delete"}), }, { @@ -174,7 +157,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"select", "insert"}, []string{"all"}), }, { @@ -183,7 +165,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"select"}), }, { @@ -192,7 +173,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"insert"}), }, { @@ -201,7 +181,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"select", "insert"}), }, { @@ -210,7 +189,6 @@ func TestDenyPermissionsForGroups(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: getAllowAllPolicyWithDenyAndExceptionForGroup([]string{"all"}, []string{"insert", "select"}), }, } diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index 1a63f79..e0b1277 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -16,10 +16,7 @@ const ( var ( testDefaultUsers = map[string]*ranger.User{ - testUserName: {ID: 11, Name: testUserName, GroupIdList: []int64{1}}, - } - testDefaultGroups = map[string]*ranger.Group{ - testGroupName: {ID: 1, Name: testGroupName}, + testUserName: {ID: 11, Name: testUserName, GroupNameList: []string{testGroupName}}, } ) @@ -29,7 +26,6 @@ type testCase struct { username string expectedResult bool users map[string]*ranger.User - groups map[string]*ranger.Group policies []*ranger.Policy } @@ -41,10 +37,7 @@ func TestRangerPolicyCheck(t *testing.T) { username: "alice", expectedResult: true, users: map[string]*ranger.User{ - "alice": {ID: 1, Name: "alice", GroupIdList: []int64{1}}, - }, - groups: map[string]*ranger.Group{ - "group1": {ID: 1, Name: "group1"}, + "alice": {ID: 1, Name: "alice", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { @@ -85,17 +78,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "bob", expectedResult: true, users: map[string]*ranger.User{ - "bob": {ID: 2, Name: "bob", GroupIdList: []int64{1}}, - }, - groups: map[string]*ranger.Group{ - "group1": {ID: 1, Name: "group1"}, + "bob": {ID: 2, Name: "bob", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 2, GUID: "policy-2", IsEnabled: true, - Name: "Allow select for group1", + Name: "Allow select for testGroupName", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -114,7 +104,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, PolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group1"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -129,10 +119,7 @@ func TestRangerPolicyCheck(t *testing.T) { username: "charlie", expectedResult: false, users: map[string]*ranger.User{ - "charlie": {ID: 3, Name: "charlie", GroupIdList: []int64{2}}, - }, - groups: map[string]*ranger.Group{ - "group2": {ID: 2, Name: "group2"}, + "charlie": {ID: 3, Name: "charlie", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { @@ -181,9 +168,8 @@ func TestRangerPolicyCheck(t *testing.T) { username: "dave", expectedResult: false, users: map[string]*ranger.User{ - "dave": {ID: 4, Name: "dave", GroupIdList: []int64{}}, + "dave": {ID: 4, Name: "dave", GroupNameList: []string{}}, }, - groups: map[string]*ranger.Group{}, policies: []*ranger.Policy{}, }, { @@ -192,10 +178,7 @@ func TestRangerPolicyCheck(t *testing.T) { username: "eve", expectedResult: false, users: map[string]*ranger.User{ - "eve": {ID: 5, Name: "eve", GroupIdList: []int64{3}}, - }, - groups: map[string]*ranger.Group{ - "group3": {ID: 3, Name: "group3"}, + "eve": {ID: 5, Name: "eve", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { @@ -251,7 +234,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, DenyPolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group3"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -266,10 +249,7 @@ func TestRangerPolicyCheck(t *testing.T) { username: "frank", expectedResult: false, users: map[string]*ranger.User{ - "frank": {ID: 6, Name: "frank", GroupIdList: []int64{4}}, - }, - groups: map[string]*ranger.Group{ - "group4": {ID: 4, Name: "group4"}, + "frank": {ID: 6, Name: "frank", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { @@ -291,7 +271,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, PolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group4"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -306,17 +286,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "grace", expectedResult: true, users: map[string]*ranger.User{ - "grace": {ID: 7, Name: "grace", GroupIdList: []int64{5}}, - }, - groups: map[string]*ranger.Group{ - "group5": {ID: 5, Name: "group5"}, + "grace": {ID: 7, Name: "grace", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 7, GUID: "policy-7", IsEnabled: true, - Name: "Allow select for group5 on tables matching regex", + Name: "Allow select for testGroupName on tables matching regex", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -335,7 +312,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, PolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group5"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -350,17 +327,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "heidi", expectedResult: false, users: map[string]*ranger.User{ - "heidi": {ID: 8, Name: "heidi", GroupIdList: []int64{6}}, - }, - groups: map[string]*ranger.Group{ - "group6": {ID: 6, Name: "group6"}, + "heidi": {ID: 8, Name: "heidi", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 8, GUID: "policy-8", IsEnabled: true, - Name: "Deny select for group6 on tables matching regex", + Name: "Deny select for testGroupName on tables matching regex", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -379,7 +353,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, DenyPolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group6"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -387,7 +361,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, PolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group6"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "*"}, }, @@ -402,17 +376,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "ivan", expectedResult: false, users: map[string]*ranger.User{ - "ivan": {ID: 9, Name: "ivan", GroupIdList: []int64{7}}, - }, - groups: map[string]*ranger.Group{ - "group7": {ID: 7, Name: "group7"}, + "ivan": {ID: 9, Name: "ivan", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 9, GUID: "policy-9", IsEnabled: true, - Name: "Allow select for group7 excluding user ivan", + Name: "Allow select for testGroupName excluding user ivan", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -431,7 +402,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, PolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group7"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -454,17 +425,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "judy", expectedResult: false, users: map[string]*ranger.User{ - "judy": {ID: 10, Name: "judy", GroupIdList: []int64{8}}, - }, - groups: map[string]*ranger.Group{ - "group8": {ID: 8, Name: "group8"}, + "judy": {ID: 10, Name: "judy", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 10, GUID: "policy-10", IsEnabled: true, - Name: "Deny select for group8 excluding user judy", + Name: "Deny select for testGroupName excluding user judy", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -491,7 +459,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, DenyPolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group8"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -506,17 +474,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "judy", expectedResult: true, users: map[string]*ranger.User{ - "judy": {ID: 10, Name: "judy", GroupIdList: []int64{8}}, - }, - groups: map[string]*ranger.Group{ - "group8": {ID: 8, Name: "group8"}, + "judy": {ID: 10, Name: "judy", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 10, GUID: "policy-10", IsEnabled: true, - Name: "Deny select for group8 excluding user judy", + Name: "Deny select for testGroupName excluding user judy", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -566,17 +531,14 @@ func TestRangerPolicyCheck(t *testing.T) { username: "kate", expectedResult: true, users: map[string]*ranger.User{ - "kate": {ID: 11, Name: "kate", GroupIdList: []int64{9}}, - }, - groups: map[string]*ranger.Group{ - "group9": {ID: 9, Name: "group9"}, + "kate": {ID: 11, Name: "kate", GroupNameList: []string{testGroupName}}, }, policies: []*ranger.Policy{ { ID: 11, GUID: "policy-11", IsEnabled: true, - Name: "Allow select for group9 excluding schema 'internal'", + Name: "Allow select for testGroupName excluding schema 'internal'", PolicyType: 0, PolicyPriority: 1, Resources: &ranger.Resource{ @@ -595,7 +557,7 @@ func TestRangerPolicyCheck(t *testing.T) { }, PolicyItems: []ranger.PolicyItem{ { - Groups: []string{"group9"}, + Groups: []string{testGroupName}, Accesses: []ranger.Access{ {Type: "select"}, }, @@ -619,7 +581,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "table2"), nil)}, }, { @@ -628,7 +589,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "private", "table1"), nil)}, }, { @@ -637,7 +597,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("not_default_catalog", "public", "table1"), nil)}, }, { @@ -646,7 +605,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("not_default_catalog", "public", "table1"), createResource("default_catalog", "public", "table2"))}, }, { @@ -655,7 +613,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_*", "public", "table1"), nil)}, }, { @@ -664,7 +621,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "p*c", "table1"), nil)}, }, { @@ -673,7 +629,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "t*l*"), nil)}, }, { @@ -682,7 +637,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "t*l*"), nil)}, }, { @@ -691,7 +645,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResource("default_catalog", "public", "table1"), nil)}, }, { @@ -700,7 +653,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("catalog", "public", "table1", true), nil)}, }, { @@ -709,7 +661,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("catalo*", "public", "table1", true), nil)}, }, { @@ -718,7 +669,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("defa*", "public", "table1", true), nil)}, }, { @@ -727,7 +677,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForCatalog("defa*", "public", "table1", true), createResource("default_catalog", "public", "table1"))}, }, { @@ -736,7 +685,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForSchema("defa*", "public", "table1", true), nil)}, }, { @@ -745,7 +693,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForSchema("defa*", "privat*", "table1", true), nil)}, }, { @@ -754,7 +701,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForSchema("defa*", "public", "table1", true), createResource("default_catalog", "public", "table1"))}, }, { @@ -763,7 +709,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForTable("defau*", "public", "table1", true), nil)}, }, { @@ -772,7 +717,6 @@ func TestResourcesSelection(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getAllowAllPolicy(createResourceWithExcludeOptionForTable("defau*", "public", "table2", true), nil)}, }, } @@ -784,9 +728,9 @@ func runTests(t *testing.T, tests []testCase) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rbac := &ranger.ApacheRanger{ + rbac := &ranger.Ranger{ AccessReceiver: trino.NewTrinoAccessReceiver("default_catalog"), - Client: getMockRangerClient(tt.users, tt.groups, tt.policies), + Client: getMockRangerClient(tt.users, tt.policies), ServiceName: serviceName, } rbac.SyncState() @@ -895,11 +839,9 @@ func getAllowAllPolicy(resource *ranger.Resource, additionalResource *ranger.Res } } - -func getMockRangerClient(users map[string]*ranger.User, groups map[string]*ranger.Group, policies []*ranger.Policy) ranger.ClientWrapper { +func getMockRangerClient(users map[string]*ranger.User, policies []*ranger.Policy) *ranger.ClientWrapper { m := new(mocks.Client) m.On("GetUsers").Return(users, nil) - m.On("GetGroups").Return(groups, nil) m.On("GetPolicies", serviceName).Return(policies, nil) - return ranger.ClientWrapper{Client: m} + return &ranger.ClientWrapper{Client: m} } diff --git a/pkg/rbac/ranger/tests/user_policy_test.go b/pkg/rbac/ranger/tests/user_policy_test.go index d256751..587892f 100644 --- a/pkg/rbac/ranger/tests/user_policy_test.go +++ b/pkg/rbac/ranger/tests/user_policy_test.go @@ -14,8 +14,8 @@ func TestDenyPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyForUser([]string{"select"}), + + policies: getAllowAllPolicyWithDenyForUser([]string{"select"}), }, { name: "Policy denies insert action for user", @@ -23,8 +23,8 @@ func TestDenyPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyForUser([]string{"insert"}), + + policies: getAllowAllPolicyWithDenyForUser([]string{"insert"}), }, { name: "Policy denies update action for user but query is select", @@ -32,8 +32,8 @@ func TestDenyPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyForUser([]string{"update"}), + + policies: getAllowAllPolicyWithDenyForUser([]string{"update"}), }, { name: "Policy denies select and insert actions for user", @@ -41,8 +41,8 @@ func TestDenyPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"select", "insert"}, []string{"all"}), + + policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"select", "insert"}, []string{"all"}), }, { name: "Policy denies all actions for user but exception allows select", @@ -50,8 +50,8 @@ func TestDenyPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, - policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"all"}, []string{"select"}), + + policies: getAllowAllPolicyWithDenyAndExceptionForUser([]string{"all"}, []string{"select"}), }, } @@ -66,7 +66,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, }, { @@ -75,7 +74,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, }, { @@ -84,7 +82,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert"})}, }, { @@ -93,7 +90,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "select", "update"})}, }, { @@ -102,7 +98,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"insert", "update", "delete"})}, }, { @@ -111,7 +106,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{}, }, { @@ -120,7 +114,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"select"})}, }, { @@ -129,8 +122,8 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, - policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, + + policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"all"})}, }, { name: "Policy many actions and many are required", @@ -138,7 +131,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultUserAllowPolicy([]string{"delete", "insert", "select", "update"})}, }, { @@ -147,7 +139,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"select"})}, }, { @@ -156,7 +147,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: false, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, }, { @@ -165,7 +155,6 @@ func TestAllowPermissionsForUser(t *testing.T) { username: testUserName, expectedResult: true, users: testDefaultUsers, - groups: testDefaultGroups, policies: []*ranger.Policy{getDefaultAllActionsUserPolicyWithExcludeForDefaultUser([]string{"insert"})}, }, } diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 7ed1a79..5c5c3b4 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -5,30 +5,37 @@ import ( "errors" "fmt" - "github.com/patterninc/heimdall/pkg/rbac/ranger" - parserFactory "github.com/patterninc/heimdall/pkg/sql/parser/factory" "gopkg.in/yaml.v3" + + "github.com/patterninc/heimdall/pkg/rbac/ranger" ) var ( ErrRBACIDsAreNotUnique = errors.New("rbac IDs are not unique") + supportedRBACs = map[string]func() RBAC{ + `apache_ranger`: NewRanger, + } ) type RBAC interface { - Init(ctx context.Context) error + Init(ctx context.Context) error //todo consider if we have init in another interface HasAccess(user string, query string) (bool, error) GetName() string } type RBACs map[string]RBAC -type RBACConfigs struct { +type configs struct { RBAC []RBAC } +// type accessReceiverHolder struct { +// AccessReceiver +// } + func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { - var temp RBACConfigs + var temp configs if err := unmarshal(&temp); err != nil { return err @@ -51,7 +58,7 @@ func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { } // Implements custom unmarshaling based on `type` field in YAML -func (c *RBACConfigs) UnmarshalYAML(value *yaml.Node) error { +func (c *configs) UnmarshalYAML(value *yaml.Node) error { for _, value := range value.Content { var probe struct { Type string `yaml:"type"` @@ -60,21 +67,19 @@ func (c *RBACConfigs) UnmarshalYAML(value *yaml.Node) error { return err } - switch probe.Type { - case "apache_ranger": - var r ranger.ApacheRanger - if err := value.Decode(&r); err != nil { - return err - } - c.RBAC = append(c.RBAC, &r) - parser, err := parserFactory.CreateParserByType(r.Parser.Type, r.Parser.DefaultCatalog) - if err != nil { - return err - } - r.AccessReceiver = parser - default: - return fmt.Errorf("unknown RBAC type: %s", probe.Type) + supportedRBAC, ok := supportedRBACs[probe.Type] + if !ok { + return fmt.Errorf("unsupported RBAC type: %s", probe.Type) } + r := supportedRBAC() + if err := value.Decode(r); err != nil { + return err + } + c.RBAC = append(c.RBAC, r) } return nil } + +func NewRanger() RBAC { + return &ranger.Ranger{} +} From ab90faa1dcbd4b122a32ac94f8a158b33770ba58 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Tue, 16 Sep 2025 10:52:38 -0600 Subject: [PATCH 13/14] More fixes --- internal/pkg/heimdall/heimdall.go | 3 +- pkg/rbac/ranger/client.go | 25 ------ pkg/rbac/ranger/ranger.go | 88 ++++++++++++++++--- .../ranger/tests/ranger_policy_check_test.go | 4 +- pkg/rbac/rbac.go | 10 +-- 5 files changed, 79 insertions(+), 51 deletions(-) diff --git a/internal/pkg/heimdall/heimdall.go b/internal/pkg/heimdall/heimdall.go index dddd274..1b46350 100644 --- a/internal/pkg/heimdall/heimdall.go +++ b/internal/pkg/heimdall/heimdall.go @@ -1,7 +1,6 @@ package heimdall import ( - "context" "fmt" "net/http" "net/http/httputil" @@ -119,7 +118,7 @@ func (h *Heimdall) Init() error { rbacsByName := map[string]rbac.RBAC{} for rbacName, r := range h.RBACs { - if err := r.Init(context.Background()); err != nil { + if err := r.Init(); err != nil { return fmt.Errorf("failed to init rbac %s: %w", rbacName, err) } rbacsByName[rbacName] = r diff --git a/pkg/rbac/ranger/client.go b/pkg/rbac/ranger/client.go index 900303e..3a17fa3 100644 --- a/pkg/rbac/ranger/client.go +++ b/pkg/rbac/ranger/client.go @@ -9,8 +9,6 @@ import ( "net/http" "strings" "time" - - "gopkg.in/yaml.v3" ) const ( @@ -24,29 +22,6 @@ type Client interface { GetPolicies(serviceName string) ([]*Policy, error) } -type ClientWrapper struct { - Client Client -} - -func (aw *ClientWrapper) UnmarshalYAML(value *yaml.Node) error { - var cl client - if err := value.Decode(&cl); err != nil { - return err - } - aw.Client = &cl - cl.client = &http.Client{} - return nil -} - -func (cw *ClientWrapper) GetUsers() (map[string]*User, error) { - return cw.Client.GetUsers() -} - - -func (cw *ClientWrapper) GetPolicies(serviceName string) ([]*Policy, error) { - return cw.Client.GetPolicies(serviceName) -} - type User struct { ID int64 `json:"id,omitempty"` Name string `json:"name,omitempty"` diff --git a/pkg/rbac/ranger/ranger.go b/pkg/rbac/ranger/ranger.go index 3cc3fee..7269927 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/pkg/rbac/ranger/ranger.go @@ -2,41 +2,57 @@ package ranger import ( "context" + "errors" "log" "strings" "time" + "gopkg.in/yaml.v3" + "github.com/patterninc/heimdall/pkg/sql/parser" + "github.com/patterninc/heimdall/pkg/sql/parser/factory" +) + +var ( + ErrRangerClientConfigIsRequired = errors.New("ranger client_config is required") + ErrRangerParserConfigIsRequired = errors.New("ranger parser_config is required") + ErrRangerParserTypeIsRequired = errors.New("ranger parser_config.type is required") + ErrRangerParserDefaultCatalogIsRequired = errors.New("ranger parser_config.default_catalog is required") + ErrRangerUnsupportedParserType = errors.New("unsupported ranger parser_config.type. supported types: trino") ) -// only private -// add links type Ranger struct { - Name string `yaml:"name,omitempty" json:"name,omitempty"` - ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` - Client *ClientWrapper `yaml:"client,omitempty" json:"client,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` + Client Client SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` AccessReceiver parser.AccessReceiver `yaml:"parser,omitempty" json:"parser,omitempty"` - permissionsByUser map[string]*UserPermissions + permissionsByUser map[string]*userPermissions } -type ParserConfig struct { +type parserConfig struct { Type string `yaml:"type,omitempty" json:"type,omitempty"` DefaultCatalog string `yaml:"default_catalog,omitempty" json:"default_catalog,omitempty"` } -type UserPermissions struct { - AllowPolicies map[parser.Action][]*Policy // todo AllowPolicies +type clientConfig struct { + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + Password string `yaml:"password,omitempty" json:"password,omitempty"` +} + +type userPermissions struct { + AllowPolicies map[parser.Action][]*Policy DenyPolicies map[parser.Action][]*Policy } -func (r *Ranger) Init(ctx context.Context) error { +func (r *Ranger) Init() error { // first time lets sync state explicitly if err := r.SyncState(); err != nil { return err } go func() { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() ticker := time.NewTicker(time.Duration(r.SyncIntervalInMinutes) * time.Minute) @@ -115,7 +131,7 @@ func (r *Ranger) SyncState() error { } } - newPermissionsByUser := map[string]*UserPermissions{} + newPermissionsByUser := map[string]*userPermissions{} for _, policy := range policies { if !policy.IsEnabled { continue @@ -133,7 +149,7 @@ func (r *Ranger) SyncState() error { controlledActions := policy.getControlledActions(usersByGroup) for userName, actions := range controlledActions.allowedActionsByUser { if _, ok := newPermissionsByUser[userName]; !ok { - newPermissionsByUser[userName] = &UserPermissions{ + newPermissionsByUser[userName] = &userPermissions{ AllowPolicies: map[parser.Action][]*Policy{}, DenyPolicies: map[parser.Action][]*Policy{}, } @@ -144,7 +160,7 @@ func (r *Ranger) SyncState() error { } for userName, actions := range controlledActions.deniedActionsByUser { if _, ok := newPermissionsByUser[userName]; !ok { - newPermissionsByUser[userName] = &UserPermissions{ + newPermissionsByUser[userName] = &userPermissions{ AllowPolicies: map[parser.Action][]*Policy{}, DenyPolicies: map[parser.Action][]*Policy{}, } @@ -159,3 +175,47 @@ func (r *Ranger) SyncState() error { log.Println("Syncing users and groups from Apache Ranger for service:", r.ServiceName) return nil } + +func (r *Ranger) UnmarshalYAML(value *yaml.Node) error { + type rawRanger struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` + SyncIntervalInMinutes int `yaml:"sync_interval_in_minutes,omitempty" json:"sync_interval_in_minutes,omitempty"` + Client *clientConfig `yaml:"client"` + Parser *parserConfig `yaml:"parser"` + } + + var raw rawRanger + if err := value.Decode(&raw); err != nil { + return err + } + + if raw.Client == nil { + return ErrRangerClientConfigIsRequired + } + if raw.Parser == nil { + return ErrRangerParserConfigIsRequired + } + if raw.Parser.Type == "" { + return ErrRangerParserTypeIsRequired + } + if raw.Parser.DefaultCatalog == "" { + return ErrRangerParserDefaultCatalogIsRequired + } + + r.Name = raw.Name + r.ServiceName = raw.ServiceName + r.SyncIntervalInMinutes = raw.SyncIntervalInMinutes + r.Client = NewClient(raw.Client.Endpoint, raw.Client.Username, raw.Client.Password) + + accessReceiver, err := factory.CreateParserByType(raw.Parser.Type, raw.Parser.DefaultCatalog) + if err != nil { + return ErrRangerUnsupportedParserType + } + + r.AccessReceiver = accessReceiver + if r.SyncIntervalInMinutes == 0 { + r.SyncIntervalInMinutes = 5 + } + return nil +} diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/pkg/rbac/ranger/tests/ranger_policy_check_test.go index e0b1277..aa3db9d 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -839,9 +839,9 @@ func getAllowAllPolicy(resource *ranger.Resource, additionalResource *ranger.Res } } -func getMockRangerClient(users map[string]*ranger.User, policies []*ranger.Policy) *ranger.ClientWrapper { +func getMockRangerClient(users map[string]*ranger.User, policies []*ranger.Policy) ranger.Client { m := new(mocks.Client) m.On("GetUsers").Return(users, nil) m.On("GetPolicies", serviceName).Return(policies, nil) - return &ranger.ClientWrapper{Client: m} + return m } diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 5c5c3b4..73ca6e8 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -1,13 +1,11 @@ package rbac import ( - "context" "errors" "fmt" - "gopkg.in/yaml.v3" - "github.com/patterninc/heimdall/pkg/rbac/ranger" + "gopkg.in/yaml.v3" ) var ( @@ -18,7 +16,7 @@ var ( ) type RBAC interface { - Init(ctx context.Context) error //todo consider if we have init in another interface + Init() error HasAccess(user string, query string) (bool, error) GetName() string } @@ -29,10 +27,6 @@ type configs struct { RBAC []RBAC } -// type accessReceiverHolder struct { -// AccessReceiver -// } - func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { var temp configs From b62a16999c2ce23b1eccc85ae37ae875fcf3b825 Mon Sep 17 00:00:00 2001 From: "ivan.hladush" Date: Thu, 30 Oct 2025 10:15:17 -0600 Subject: [PATCH 14/14] Small refactoring --- build.sh | 2 +- cmd/heimdall/heimdall.go | 30 +++---- configs/local.yaml | 37 +++++++-- internal/pkg/heimdall/auth.go | 8 +- internal/pkg/heimdall/heimdall.go | 34 ++++---- internal/pkg/heimdall/plugins.go | 1 + {pkg => internal/pkg}/rbac/ranger/client.go | 0 .../pkg}/rbac/ranger/mocks/Client.go | 2 +- {pkg => internal/pkg}/rbac/ranger/policy.go | 2 +- {pkg => internal/pkg}/rbac/ranger/ranger.go | 4 +- .../rbac/ranger/tests/group_policy_test.go | 2 +- .../ranger/tests/ranger_policy_check_test.go | 6 +- .../rbac/ranger/tests/user_policy_test.go | 2 +- internal/pkg/rbac/rbac.go | 79 +++++++++++++++++++ {pkg => internal/pkg}/sql/parser/README.md | 0 .../pkg}/sql/parser/factory/factory.go | 4 +- {pkg => internal/pkg}/sql/parser/sql.go | 0 .../pkg}/sql/parser/trino/TrinoLexer.g4 | 0 .../pkg}/sql/parser/trino/TrinoParser.g4 | 0 .../parser/trino/grammar/TrinoLexer.interp | 0 .../parser/trino/grammar/TrinoLexer.tokens | 0 .../parser/trino/grammar/TrinoParser.interp | 0 .../parser/trino/grammar/TrinoParser.tokens | 0 .../sql/parser/trino/grammar/trino_lexer.go | 0 .../sql/parser/trino/grammar/trino_parser.go | 0 .../grammar/trinoparser_base_listener.go | 0 .../trino/grammar/trinoparser_base_visitor.go | 0 .../trino/grammar/trinoparser_listener.go | 0 .../trino/grammar/trinoparser_visitor.go | 0 .../pkg}/sql/parser/trino/listener.go | 4 +- .../pkg}/sql/parser/trino/parser.go | 4 +- .../pkg}/sql/parser/trino/tests/alter_test.go | 2 +- .../sql/parser/trino/tests/create_test.go | 2 +- .../sql/parser/trino/tests/delete_test.go | 2 +- .../pkg}/sql/parser/trino/tests/drop_test.go | 2 +- .../sql/parser/trino/tests/insert_test.go | 2 +- .../sql/parser/trino/tests/merge_into_test.go | 2 +- .../sql/parser/trino/tests/select_test.go | 2 +- .../sql/parser/trino/tests/update_test.go | 2 +- .../parser/trino/unused_listener_functions.go | 2 +- pkg/rbac/README.md | 14 +--- pkg/rbac/rbac.go | 72 ----------------- 42 files changed, 170 insertions(+), 155 deletions(-) rename {pkg => internal/pkg}/rbac/ranger/client.go (100%) rename {pkg => internal/pkg}/rbac/ranger/mocks/Client.go (96%) rename {pkg => internal/pkg}/rbac/ranger/policy.go (99%) rename {pkg => internal/pkg}/rbac/ranger/ranger.go (98%) rename {pkg => internal/pkg}/rbac/ranger/tests/group_policy_test.go (99%) rename {pkg => internal/pkg}/rbac/ranger/tests/ranger_policy_check_test.go (99%) rename {pkg => internal/pkg}/rbac/ranger/tests/user_policy_test.go (99%) create mode 100644 internal/pkg/rbac/rbac.go rename {pkg => internal/pkg}/sql/parser/README.md (100%) rename {pkg => internal/pkg}/sql/parser/factory/factory.go (75%) rename {pkg => internal/pkg}/sql/parser/sql.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/TrinoLexer.g4 (100%) rename {pkg => internal/pkg}/sql/parser/trino/TrinoParser.g4 (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/TrinoLexer.interp (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/TrinoLexer.tokens (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/TrinoParser.interp (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/TrinoParser.tokens (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/trino_lexer.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/trino_parser.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/trinoparser_base_listener.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/trinoparser_base_visitor.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/trinoparser_listener.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/grammar/trinoparser_visitor.go (100%) rename {pkg => internal/pkg}/sql/parser/trino/listener.go (95%) rename {pkg => internal/pkg}/sql/parser/trino/parser.go (84%) rename {pkg => internal/pkg}/sql/parser/trino/tests/alter_test.go (96%) rename {pkg => internal/pkg}/sql/parser/trino/tests/create_test.go (96%) rename {pkg => internal/pkg}/sql/parser/trino/tests/delete_test.go (98%) rename {pkg => internal/pkg}/sql/parser/trino/tests/drop_test.go (95%) rename {pkg => internal/pkg}/sql/parser/trino/tests/insert_test.go (98%) rename {pkg => internal/pkg}/sql/parser/trino/tests/merge_into_test.go (97%) rename {pkg => internal/pkg}/sql/parser/trino/tests/select_test.go (99%) rename {pkg => internal/pkg}/sql/parser/trino/tests/update_test.go (97%) rename {pkg => internal/pkg}/sql/parser/trino/unused_listener_functions.go (99%) diff --git a/build.sh b/build.sh index 790dc87..c208212 100755 --- a/build.sh +++ b/build.sh @@ -28,4 +28,4 @@ mkdir -p ${OUTPUT_DIR}/web ${OUTPUT_DIR}/plugins (cd ${WORKING_DIR} && for item in ${PLUGINS}; do go build -buildmode=plugin -ldflags "${LDFLAGS}" -o dist/plugins/$item.so plugins/$item/$item.go; done) # build web -(cd ${WORKING_DIR}/web && rm -rf node_modules > /dev/null 2>&1 && corepack enable && pnpm install --frozen-lockfile --ignore-scripts && pnpm run build) +# (cd ${WORKING_DIR}/web && rm -rf node_modules > /dev/null 2>&1 && corepack enable && pnpm install --frozen-lockfile --ignore-scripts && pnpm run build) diff --git a/cmd/heimdall/heimdall.go b/cmd/heimdall/heimdall.go index 08bef3b..5387357 100644 --- a/cmd/heimdall/heimdall.go +++ b/cmd/heimdall/heimdall.go @@ -8,7 +8,7 @@ import ( "github.com/patterninc/heimdall/internal/pkg/heimdall" "github.com/patterninc/heimdall/internal/pkg/janitor" - "github.com/patterninc/heimdall/internal/pkg/server" + // "github.com/patterninc/heimdall/internal/pkg/server" ) const ( @@ -38,13 +38,13 @@ func main() { // setup defaults before we unmarshal config h := heimdall.Heimdall{ - Server: &server.Server{ - Address: defaultAddress, - ReadTimeout: defaultReadTimeout, - WriteTimeout: defaulWriteTimeout, - IdleTimeout: defaultIdleTimeout, - ReadHeaderTimeout: defaultReadHeaderTimeout, - }, + // Server: &server.Server{ + // Address: defaultAddress, + // ReadTimeout: defaultReadTimeout, + // WriteTimeout: defaulWriteTimeout, + // IdleTimeout: defaultIdleTimeout, + // ReadHeaderTimeout: defaultReadHeaderTimeout, + // }, Janitor: &janitor.Janitor{ Keepalive: defaultJanitorKeepalive, StaleJob: defaultStaleJob, @@ -52,16 +52,16 @@ func main() { } // load config file - if err := config.LoadYAML(configFile, &h); err != nil { + if err := config.LoadYAML("/Users/ivanhladush/git/heimdall/configs/local.yaml", &h); err != nil { process.Bail(`config`, err) } - // setup version - if Build != `` { - h.Version = Build - } else { - h.Version = defaultBuild - } + // // setup version + // if Build != `` { + // h.Version = Build + // } else { + // h.Version = defaultBuild + // } // start proxy if err := h.Start(); err != nil { diff --git a/configs/local.yaml b/configs/local.yaml index b420f30..5cc8f6d 100644 --- a/configs/local.yaml +++ b/configs/local.yaml @@ -19,16 +19,27 @@ auth: # supported commands commands: - - name: ping-0.0.1 + # # - name: ping-0.0.1 + # status: active + # plugin: ping + # version: 0.0.1 + # store_result_sync: false + # description: Test ping command + # tags: + # - type:ping + # cluster_tags: + # - type:localhost + - name: trino-475 status: active - plugin: ping - version: 0.0.1 - store_result_sync: false - description: Test ping command + plugin: trino + version: 475 + description: Run Trino queries + context: + poll_interval: 250 # milliseconds between poll attempts tags: - - type:ping + - type:trino cluster_tags: - - type:localhost + - type:trino # supported clusters clusters: @@ -38,4 +49,14 @@ clusters: description: Just a localhost tags: - type:localhost - - data:local \ No newline at end of file + - data:local + - name: eks-trino-475 + status: active + version: 475 + description: Trino cluster in us-west-2 + tags: + - type:trino + - data:prod + rbacs: + - my-ranger + diff --git a/internal/pkg/heimdall/auth.go b/internal/pkg/heimdall/auth.go index e635cbf..c52cce9 100644 --- a/internal/pkg/heimdall/auth.go +++ b/internal/pkg/heimdall/auth.go @@ -28,10 +28,10 @@ func (h *Heimdall) auth(next http.Handler) http.Handler { // let's get username from the header username := `` - if h.Auth != nil { - // TODO: process error here... - username, _ = h.Auth.GetUser(r) - } + // if h.Auth != nil { + // // TODO: process error here... + // username, _ = h.Auth.GetUser(r) + // } // let's write this username to request context... ctx := context.WithValue(r.Context(), userNameKey, username) diff --git a/internal/pkg/heimdall/heimdall.go b/internal/pkg/heimdall/heimdall.go index 1b46350..15107cd 100644 --- a/internal/pkg/heimdall/heimdall.go +++ b/internal/pkg/heimdall/heimdall.go @@ -11,7 +11,6 @@ import ( "github.com/gorilla/mux" - "github.com/patterninc/heimdall/internal/pkg/auth" "github.com/patterninc/heimdall/internal/pkg/database" "github.com/patterninc/heimdall/internal/pkg/janitor" "github.com/patterninc/heimdall/internal/pkg/pool" @@ -20,7 +19,7 @@ import ( "github.com/patterninc/heimdall/pkg/object/command" "github.com/patterninc/heimdall/pkg/object/job" "github.com/patterninc/heimdall/pkg/plugin" - "github.com/patterninc/heimdall/pkg/rbac" + "github.com/patterninc/heimdall/internal/pkg/rbac" ) const ( @@ -41,20 +40,20 @@ const ( type Heimdall struct { Server *server.Server `yaml:"server,omitempty" json:"server,omitempty"` - Commands command.Commands `yaml:"commands,omitempty" json:"commands,omitempty"` - Clusters cluster.Clusters `yaml:"clusters,omitempty" json:"clusters,omitempty"` - RBACs rbac.RBACs `yaml:"rbacs,omitempty" json:"rbacs,omitempty"` - JobsDirectory string `yaml:"jobs_directory,omitempty" json:"jobs_directory,omitempty"` - ArchiveDirectory string `yaml:"archive_directory,omitempty" json:"archive_directory,omitempty"` - ResultDirectory string `yaml:"result_directory,omitempty" json:"result_directory,omitempty"` - PluginsDirectory string `yaml:"plugin_directory,omitempty" json:"plugin_directory,omitempty"` - Database *database.Database `yaml:"database,omitempty" json:"database,omitempty"` + Commands command.Commands `yaml:"commands,omitempty" json:"commands,omitempty"` + Clusters cluster.Clusters `yaml:"clusters,omitempty" json:"clusters,omitempty"` + RBACs rbac.RBACs `yaml:"rbacs,omitempty" json:"rbacs,omitempty"` + JobsDirectory string `yaml:"jobs_directory,omitempty" json:"jobs_directory,omitempty"` + ArchiveDirectory string `yaml:"archive_directory,omitempty" json:"archive_directory,omitempty"` + ResultDirectory string `yaml:"result_directory,omitempty" json:"result_directory,omitempty"` + PluginsDirectory string `yaml:"plugin_directory,omitempty" json:"plugin_directory,omitempty"` + Database *database.Database `yaml:"database,omitempty" json:"database,omitempty"` Pool *pool.Pool[*job.Job] `yaml:"pool,omitempty" json:"pool,omitempty"` Auth *auth.Auth `yaml:"auth,omitempty" json:"auth,omitempty"` - Janitor *janitor.Janitor `yaml:"janitor,omitempty" json:"janitor,omitempty"` - Version string `yaml:"-" json:"-"` - agentName string - commandHandlers map[string]plugin.Handler + Janitor *janitor.Janitor `yaml:"janitor,omitempty" json:"janitor,omitempty"` + Version string `yaml:"-" json:"-"` + agentName string + commandHandlers map[string]plugin.Handler } func (h *Heimdall) Init() error { @@ -64,12 +63,12 @@ func (h *Heimdall) Init() error { h.JobsDirectory = defaultJobsDirectory } - // set archive directory if not set + // // set archive directory if not set if h.ArchiveDirectory == `` { h.ArchiveDirectory = defaultArchiveDirectory } - // set result directory if not set + // // set result directory if not set if h.ResultDirectory == `` { h.ResultDirectory = defaultResultDirectory } @@ -146,14 +145,13 @@ func (h *Heimdall) Init() error { } } - // start janitor + // // start janitor if err := h.Janitor.Start(h.Database); err != nil { return err } // let's start the agent return h.Pool.Start(h.runAsyncJob, h.getAsyncJobs) - } func (h *Heimdall) Start() error { diff --git a/internal/pkg/heimdall/plugins.go b/internal/pkg/heimdall/plugins.go index 89213df..f934d85 100644 --- a/internal/pkg/heimdall/plugins.go +++ b/internal/pkg/heimdall/plugins.go @@ -18,6 +18,7 @@ const ( func (h *Heimdall) loadPlugins() (map[string]func(*context.Context) (hp.Handler, error), error) { + return nil,nil plugins := make(map[string]func(*context.Context) (hp.Handler, error)) files, err := os.ReadDir(h.PluginsDirectory) diff --git a/pkg/rbac/ranger/client.go b/internal/pkg/rbac/ranger/client.go similarity index 100% rename from pkg/rbac/ranger/client.go rename to internal/pkg/rbac/ranger/client.go diff --git a/pkg/rbac/ranger/mocks/Client.go b/internal/pkg/rbac/ranger/mocks/Client.go similarity index 96% rename from pkg/rbac/ranger/mocks/Client.go rename to internal/pkg/rbac/ranger/mocks/Client.go index 5ce3ac6..0bd7025 100644 --- a/pkg/rbac/ranger/mocks/Client.go +++ b/internal/pkg/rbac/ranger/mocks/Client.go @@ -3,7 +3,7 @@ package mocks import ( - ranger "github.com/patterninc/heimdall/pkg/rbac/ranger" + ranger "github.com/patterninc/heimdall/internal/pkg/rbac/ranger" mock "github.com/stretchr/testify/mock" ) diff --git a/pkg/rbac/ranger/policy.go b/internal/pkg/rbac/ranger/policy.go similarity index 99% rename from pkg/rbac/ranger/policy.go rename to internal/pkg/rbac/ranger/policy.go index 52221db..38aac8a 100644 --- a/pkg/rbac/ranger/policy.go +++ b/internal/pkg/rbac/ranger/policy.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/patterninc/heimdall/pkg/sql/parser" + "github.com/patterninc/heimdall/internal/pkg/sql/parser" ) const ( diff --git a/pkg/rbac/ranger/ranger.go b/internal/pkg/rbac/ranger/ranger.go similarity index 98% rename from pkg/rbac/ranger/ranger.go rename to internal/pkg/rbac/ranger/ranger.go index 7269927..25ccfcf 100644 --- a/pkg/rbac/ranger/ranger.go +++ b/internal/pkg/rbac/ranger/ranger.go @@ -9,8 +9,8 @@ import ( "gopkg.in/yaml.v3" - "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/factory" + "github.com/patterninc/heimdall/internal/pkg/sql/parser" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/factory" ) var ( diff --git a/pkg/rbac/ranger/tests/group_policy_test.go b/internal/pkg/rbac/ranger/tests/group_policy_test.go similarity index 99% rename from pkg/rbac/ranger/tests/group_policy_test.go rename to internal/pkg/rbac/ranger/tests/group_policy_test.go index a816717..caa9a4a 100644 --- a/pkg/rbac/ranger/tests/group_policy_test.go +++ b/internal/pkg/rbac/ranger/tests/group_policy_test.go @@ -3,7 +3,7 @@ package tests import ( "testing" - "github.com/patterninc/heimdall/pkg/rbac/ranger" + "github.com/patterninc/heimdall/internal/pkg/rbac/ranger" ) func TestAllowPermissionsForGroups(t *testing.T) { diff --git a/pkg/rbac/ranger/tests/ranger_policy_check_test.go b/internal/pkg/rbac/ranger/tests/ranger_policy_check_test.go similarity index 99% rename from pkg/rbac/ranger/tests/ranger_policy_check_test.go rename to internal/pkg/rbac/ranger/tests/ranger_policy_check_test.go index aa3db9d..3bc169e 100644 --- a/pkg/rbac/ranger/tests/ranger_policy_check_test.go +++ b/internal/pkg/rbac/ranger/tests/ranger_policy_check_test.go @@ -3,9 +3,9 @@ package tests import ( "testing" - "github.com/patterninc/heimdall/pkg/rbac/ranger" - "github.com/patterninc/heimdall/pkg/rbac/ranger/mocks" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" + "github.com/patterninc/heimdall/internal/pkg/rbac/ranger" + "github.com/patterninc/heimdall/internal/pkg/rbac/ranger/mocks" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" ) const ( diff --git a/pkg/rbac/ranger/tests/user_policy_test.go b/internal/pkg/rbac/ranger/tests/user_policy_test.go similarity index 99% rename from pkg/rbac/ranger/tests/user_policy_test.go rename to internal/pkg/rbac/ranger/tests/user_policy_test.go index 587892f..bd2d92d 100644 --- a/pkg/rbac/ranger/tests/user_policy_test.go +++ b/internal/pkg/rbac/ranger/tests/user_policy_test.go @@ -3,7 +3,7 @@ package tests import ( "testing" - "github.com/patterninc/heimdall/pkg/rbac/ranger" + "github.com/patterninc/heimdall/internal/pkg/rbac/ranger" ) func TestDenyPermissionsForUser(t *testing.T) { diff --git a/internal/pkg/rbac/rbac.go b/internal/pkg/rbac/rbac.go new file mode 100644 index 0000000..d4cd19e --- /dev/null +++ b/internal/pkg/rbac/rbac.go @@ -0,0 +1,79 @@ +package rbac + +import ( + "errors" + "fmt" + + "github.com/patterninc/heimdall/internal/pkg/rbac/ranger" + "gopkg.in/yaml.v3" +) + +var ( + ErrRBACIDsAreNotUnique = errors.New("rbac IDs are not unique") + supportedRBACs = map[string]func() RBAC{ + `apache_ranger`: NewRanger, + } +) + +type RBAC interface { + Init() error + HasAccess(user string, query string) (bool, error) + GetName() string +} + +type RBACs map[string]RBAC + +type configs struct { + RBAC []RBAC +} + +func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { + + var temp configs + + if err := unmarshal(&temp); err != nil { + return err + } + + items := make(map[string]RBAC) + + for _, t := range temp.RBAC { + items[t.GetName()] = t + } + + if len(temp.RBAC) != len(items) { + return ErrRBACIDsAreNotUnique + } + + *c = items + + return nil + +} + +// Implements custom unmarshaling based on `type` field in YAML +func (c *configs) UnmarshalYAML(value *yaml.Node) error { + for _, value := range value.Content { + var probe struct { + Type string `yaml:"type"` + } + if err := value.Decode(&probe); err != nil { + return err + } + + supportedRBAC, ok := supportedRBACs[probe.Type] + if !ok { + return fmt.Errorf("unsupported RBAC type: %s", probe.Type) + } + r := supportedRBAC() + if err := value.Decode(r); err != nil { + return err + } + c.RBAC = append(c.RBAC, r) + } + return nil +} + +func NewRanger() RBAC { + return &ranger.Ranger{} +} diff --git a/pkg/sql/parser/README.md b/internal/pkg/sql/parser/README.md similarity index 100% rename from pkg/sql/parser/README.md rename to internal/pkg/sql/parser/README.md diff --git a/pkg/sql/parser/factory/factory.go b/internal/pkg/sql/parser/factory/factory.go similarity index 75% rename from pkg/sql/parser/factory/factory.go rename to internal/pkg/sql/parser/factory/factory.go index 3816bcb..5fd5c3c 100644 --- a/pkg/sql/parser/factory/factory.go +++ b/internal/pkg/sql/parser/factory/factory.go @@ -3,8 +3,8 @@ package factory import ( "fmt" - "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" + "github.com/patterninc/heimdall/internal/pkg/sql/parser" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" ) type ParserType string diff --git a/pkg/sql/parser/sql.go b/internal/pkg/sql/parser/sql.go similarity index 100% rename from pkg/sql/parser/sql.go rename to internal/pkg/sql/parser/sql.go diff --git a/pkg/sql/parser/trino/TrinoLexer.g4 b/internal/pkg/sql/parser/trino/TrinoLexer.g4 similarity index 100% rename from pkg/sql/parser/trino/TrinoLexer.g4 rename to internal/pkg/sql/parser/trino/TrinoLexer.g4 diff --git a/pkg/sql/parser/trino/TrinoParser.g4 b/internal/pkg/sql/parser/trino/TrinoParser.g4 similarity index 100% rename from pkg/sql/parser/trino/TrinoParser.g4 rename to internal/pkg/sql/parser/trino/TrinoParser.g4 diff --git a/pkg/sql/parser/trino/grammar/TrinoLexer.interp b/internal/pkg/sql/parser/trino/grammar/TrinoLexer.interp similarity index 100% rename from pkg/sql/parser/trino/grammar/TrinoLexer.interp rename to internal/pkg/sql/parser/trino/grammar/TrinoLexer.interp diff --git a/pkg/sql/parser/trino/grammar/TrinoLexer.tokens b/internal/pkg/sql/parser/trino/grammar/TrinoLexer.tokens similarity index 100% rename from pkg/sql/parser/trino/grammar/TrinoLexer.tokens rename to internal/pkg/sql/parser/trino/grammar/TrinoLexer.tokens diff --git a/pkg/sql/parser/trino/grammar/TrinoParser.interp b/internal/pkg/sql/parser/trino/grammar/TrinoParser.interp similarity index 100% rename from pkg/sql/parser/trino/grammar/TrinoParser.interp rename to internal/pkg/sql/parser/trino/grammar/TrinoParser.interp diff --git a/pkg/sql/parser/trino/grammar/TrinoParser.tokens b/internal/pkg/sql/parser/trino/grammar/TrinoParser.tokens similarity index 100% rename from pkg/sql/parser/trino/grammar/TrinoParser.tokens rename to internal/pkg/sql/parser/trino/grammar/TrinoParser.tokens diff --git a/pkg/sql/parser/trino/grammar/trino_lexer.go b/internal/pkg/sql/parser/trino/grammar/trino_lexer.go similarity index 100% rename from pkg/sql/parser/trino/grammar/trino_lexer.go rename to internal/pkg/sql/parser/trino/grammar/trino_lexer.go diff --git a/pkg/sql/parser/trino/grammar/trino_parser.go b/internal/pkg/sql/parser/trino/grammar/trino_parser.go similarity index 100% rename from pkg/sql/parser/trino/grammar/trino_parser.go rename to internal/pkg/sql/parser/trino/grammar/trino_parser.go diff --git a/pkg/sql/parser/trino/grammar/trinoparser_base_listener.go b/internal/pkg/sql/parser/trino/grammar/trinoparser_base_listener.go similarity index 100% rename from pkg/sql/parser/trino/grammar/trinoparser_base_listener.go rename to internal/pkg/sql/parser/trino/grammar/trinoparser_base_listener.go diff --git a/pkg/sql/parser/trino/grammar/trinoparser_base_visitor.go b/internal/pkg/sql/parser/trino/grammar/trinoparser_base_visitor.go similarity index 100% rename from pkg/sql/parser/trino/grammar/trinoparser_base_visitor.go rename to internal/pkg/sql/parser/trino/grammar/trinoparser_base_visitor.go diff --git a/pkg/sql/parser/trino/grammar/trinoparser_listener.go b/internal/pkg/sql/parser/trino/grammar/trinoparser_listener.go similarity index 100% rename from pkg/sql/parser/trino/grammar/trinoparser_listener.go rename to internal/pkg/sql/parser/trino/grammar/trinoparser_listener.go diff --git a/pkg/sql/parser/trino/grammar/trinoparser_visitor.go b/internal/pkg/sql/parser/trino/grammar/trinoparser_visitor.go similarity index 100% rename from pkg/sql/parser/trino/grammar/trinoparser_visitor.go rename to internal/pkg/sql/parser/trino/grammar/trinoparser_visitor.go diff --git a/pkg/sql/parser/trino/listener.go b/internal/pkg/sql/parser/trino/listener.go similarity index 95% rename from pkg/sql/parser/trino/listener.go rename to internal/pkg/sql/parser/trino/listener.go index bce0367..27682af 100644 --- a/pkg/sql/parser/trino/listener.go +++ b/internal/pkg/sql/parser/trino/listener.go @@ -1,8 +1,8 @@ package trino import ( - "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino/grammar" + "github.com/patterninc/heimdall/internal/pkg/sql/parser" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino/grammar" ) type trinoListener struct { diff --git a/pkg/sql/parser/trino/parser.go b/internal/pkg/sql/parser/trino/parser.go similarity index 84% rename from pkg/sql/parser/trino/parser.go rename to internal/pkg/sql/parser/trino/parser.go index d3171fe..b08cfb9 100644 --- a/pkg/sql/parser/trino/parser.go +++ b/internal/pkg/sql/parser/trino/parser.go @@ -4,8 +4,8 @@ import ( "log" "github.com/antlr4-go/antlr/v4" - "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino/grammar" + "github.com/patterninc/heimdall/internal/pkg/sql/parser" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino/grammar" ) type TrinoAccessReceiver struct { diff --git a/pkg/sql/parser/trino/tests/alter_test.go b/internal/pkg/sql/parser/trino/tests/alter_test.go similarity index 96% rename from pkg/sql/parser/trino/tests/alter_test.go rename to internal/pkg/sql/parser/trino/tests/alter_test.go index 58be4e6..6d05be2 100644 --- a/pkg/sql/parser/trino/tests/alter_test.go +++ b/internal/pkg/sql/parser/trino/tests/alter_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" ) func TestParseSQLAlter(t *testing.T) { diff --git a/pkg/sql/parser/trino/tests/create_test.go b/internal/pkg/sql/parser/trino/tests/create_test.go similarity index 96% rename from pkg/sql/parser/trino/tests/create_test.go rename to internal/pkg/sql/parser/trino/tests/create_test.go index 8b01829..9af5883 100644 --- a/pkg/sql/parser/trino/tests/create_test.go +++ b/internal/pkg/sql/parser/trino/tests/create_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" ) func TestParseSQLCreate(t *testing.T) { diff --git a/pkg/sql/parser/trino/tests/delete_test.go b/internal/pkg/sql/parser/trino/tests/delete_test.go similarity index 98% rename from pkg/sql/parser/trino/tests/delete_test.go rename to internal/pkg/sql/parser/trino/tests/delete_test.go index 37eebc8..d0dd49b 100644 --- a/pkg/sql/parser/trino/tests/delete_test.go +++ b/internal/pkg/sql/parser/trino/tests/delete_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" ) func TestParseSQLDelete(t *testing.T) { diff --git a/pkg/sql/parser/trino/tests/drop_test.go b/internal/pkg/sql/parser/trino/tests/drop_test.go similarity index 95% rename from pkg/sql/parser/trino/tests/drop_test.go rename to internal/pkg/sql/parser/trino/tests/drop_test.go index 1eb1a3f..739d4f5 100644 --- a/pkg/sql/parser/trino/tests/drop_test.go +++ b/internal/pkg/sql/parser/trino/tests/drop_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" ) func TestParseSQLDrop(t *testing.T) { diff --git a/pkg/sql/parser/trino/tests/insert_test.go b/internal/pkg/sql/parser/trino/tests/insert_test.go similarity index 98% rename from pkg/sql/parser/trino/tests/insert_test.go rename to internal/pkg/sql/parser/trino/tests/insert_test.go index 67c6a32..7cd10b2 100644 --- a/pkg/sql/parser/trino/tests/insert_test.go +++ b/internal/pkg/sql/parser/trino/tests/insert_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" ) func TestParseSQLInsert(t *testing.T) { diff --git a/pkg/sql/parser/trino/tests/merge_into_test.go b/internal/pkg/sql/parser/trino/tests/merge_into_test.go similarity index 97% rename from pkg/sql/parser/trino/tests/merge_into_test.go rename to internal/pkg/sql/parser/trino/tests/merge_into_test.go index 2b03999..5a2ce90 100644 --- a/pkg/sql/parser/trino/tests/merge_into_test.go +++ b/internal/pkg/sql/parser/trino/tests/merge_into_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" ) func TestParseSQLMergeInto(t *testing.T) { diff --git a/pkg/sql/parser/trino/tests/select_test.go b/internal/pkg/sql/parser/trino/tests/select_test.go similarity index 99% rename from pkg/sql/parser/trino/tests/select_test.go rename to internal/pkg/sql/parser/trino/tests/select_test.go index ff9b0dc..887f534 100644 --- a/pkg/sql/parser/trino/tests/select_test.go +++ b/internal/pkg/sql/parser/trino/tests/select_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" ) const ( diff --git a/pkg/sql/parser/trino/tests/update_test.go b/internal/pkg/sql/parser/trino/tests/update_test.go similarity index 97% rename from pkg/sql/parser/trino/tests/update_test.go rename to internal/pkg/sql/parser/trino/tests/update_test.go index 25968b2..3118387 100644 --- a/pkg/sql/parser/trino/tests/update_test.go +++ b/internal/pkg/sql/parser/trino/tests/update_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino" "github.com/patterninc/heimdall/pkg/sql/parser" - "github.com/patterninc/heimdall/pkg/sql/parser/trino" ) func TestParseSQLUpdate(t *testing.T) { diff --git a/pkg/sql/parser/trino/unused_listener_functions.go b/internal/pkg/sql/parser/trino/unused_listener_functions.go similarity index 99% rename from pkg/sql/parser/trino/unused_listener_functions.go rename to internal/pkg/sql/parser/trino/unused_listener_functions.go index 39a829a..a859f67 100644 --- a/pkg/sql/parser/trino/unused_listener_functions.go +++ b/internal/pkg/sql/parser/trino/unused_listener_functions.go @@ -4,7 +4,7 @@ import ( "log" "reflect" - "github.com/patterninc/heimdall/pkg/sql/parser/trino/grammar" + "github.com/patterninc/heimdall/internal/pkg/sql/parser/trino/grammar" ) // EnterParse is called when production parse is entered. diff --git a/pkg/rbac/README.md b/pkg/rbac/README.md index 4666f6e..b1eac5b 100644 --- a/pkg/rbac/README.md +++ b/pkg/rbac/README.md @@ -17,14 +17,11 @@ The module consists of several key components: ### Core Interfaces - **`RBAC`**: Main interface for access control providers -- **`Client`**: Interface for communicating with external systems (Apache Ranger) - ### Apache Ranger Integration The module currently supports Apache Ranger as the primary RBAC provider through: - **`ApacheRanger`**: Main implementation of RBAC interface -- **`client`**: HTTP client for Ranger API communication - **`Policy`**: Represents Ranger policies with resources and permissions - **`User`** and **`Group`**: Represent Ranger users and groups @@ -33,7 +30,7 @@ The module currently supports Apache Ranger as the primary RBAC provider through ### YAML Configuration Example ```yaml -rbac: +rbacs: - type: apache_ranger name: my-ranger service_name: my_service @@ -165,15 +162,6 @@ type RBAC interface { } ``` -### Client Interface - -```go -type Client interface { - GetUsers() (map[string]*User, error) - GetGroups() (map[string]*Group, error) - GetPolicies(serviceName string) ([]*Policy, error) -} -``` ## Error Handling diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 73ca6e8..53e43fd 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -1,79 +1,7 @@ package rbac -import ( - "errors" - "fmt" - - "github.com/patterninc/heimdall/pkg/rbac/ranger" - "gopkg.in/yaml.v3" -) - -var ( - ErrRBACIDsAreNotUnique = errors.New("rbac IDs are not unique") - supportedRBACs = map[string]func() RBAC{ - `apache_ranger`: NewRanger, - } -) - type RBAC interface { Init() error HasAccess(user string, query string) (bool, error) GetName() string } - -type RBACs map[string]RBAC - -type configs struct { - RBAC []RBAC -} - -func (c *RBACs) UnmarshalYAML(unmarshal func(interface{}) error) error { - - var temp configs - - if err := unmarshal(&temp); err != nil { - return err - } - - items := make(map[string]RBAC) - - for _, t := range temp.RBAC { - items[t.GetName()] = t - } - - if len(temp.RBAC) != len(items) { - return ErrRBACIDsAreNotUnique - } - - *c = items - - return nil - -} - -// Implements custom unmarshaling based on `type` field in YAML -func (c *configs) UnmarshalYAML(value *yaml.Node) error { - for _, value := range value.Content { - var probe struct { - Type string `yaml:"type"` - } - if err := value.Decode(&probe); err != nil { - return err - } - - supportedRBAC, ok := supportedRBACs[probe.Type] - if !ok { - return fmt.Errorf("unsupported RBAC type: %s", probe.Type) - } - r := supportedRBAC() - if err := value.Decode(r); err != nil { - return err - } - c.RBAC = append(c.RBAC, r) - } - return nil -} - -func NewRanger() RBAC { - return &ranger.Ranger{} -}