From 614d52b20c3ba71df13a7ee723e13dd5d7f2a7cc Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Fri, 13 Oct 2023 13:51:33 +0200 Subject: [PATCH] feat(opensearch): implement support for opensearch acls Fixes #315 --- client.go | 2 + elasticsearch_acls.go | 92 ++++++--- opensearch_acls.go | 151 +++++++++++++++ opensearch_acls_test.go | 405 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 619 insertions(+), 31 deletions(-) create mode 100644 opensearch_acls.go create mode 100644 opensearch_acls_test.go diff --git a/client.go b/client.go index 2e6a6b6..442ca34 100644 --- a/client.go +++ b/client.go @@ -95,6 +95,7 @@ type Client struct { OrganizationUserGroups *OrganizationUserGroupHandler OrganizationUserGroupMembers *OrganizationUserGroupMembersHandler OpenSearchSecurityPluginHandler *OpenSearchSecurityPluginHandler + OpenSearchACLs *OpenSearchACLsHandler } // GetUserAgentOrDefault configures a default userAgent value, if one has not been provided. @@ -270,6 +271,7 @@ func (c *Client) Init() { c.OrganizationUserGroups = &OrganizationUserGroupHandler{c} c.OrganizationUserGroupMembers = &OrganizationUserGroupMembersHandler{c} c.OpenSearchSecurityPluginHandler = &OpenSearchSecurityPluginHandler{c} + c.OpenSearchACLs = &OpenSearchACLsHandler{c} } func (c *Client) doGetRequest(ctx context.Context, endpoint string, req interface{}) ([]byte, error) { diff --git a/elasticsearch_acls.go b/elasticsearch_acls.go index a8e36a9..e670184 100644 --- a/elasticsearch_acls.go +++ b/elasticsearch_acls.go @@ -42,6 +42,8 @@ type ( ) // Update updates Elasticsearch ACL config +// +// Deprecated: Use OpenSearchACLsHandler.Update instead. func (h *ElasticSearchACLsHandler) Update(ctx context.Context, project, service string, req ElasticsearchACLRequest) (*ElasticSearchACLResponse, error) { path := buildPath("project", project, "service", service, "elasticsearch", "acl") bts, err := h.client.doPutRequest(ctx, path, req) @@ -50,12 +52,12 @@ func (h *ElasticSearchACLsHandler) Update(ctx context.Context, project, service } var r ElasticSearchACLResponse - errR := checkAPIResponse(bts, &r) - - return &r, errR + return &r, checkAPIResponse(bts, &r) } // Get gets all existing Elasticsearch ACLs config +// +// Deprecated: Use OpenSearchACLsHandler.Get instead. func (h *ElasticSearchACLsHandler) Get(ctx context.Context, project, service string) (*ElasticSearchACLResponse, error) { path := buildPath("project", project, "service", service, "elasticsearch", "acl") bts, err := h.client.doGetRequest(ctx, path, nil) @@ -64,62 +66,90 @@ func (h *ElasticSearchACLsHandler) Get(ctx context.Context, project, service str } var r ElasticSearchACLResponse - errR := checkAPIResponse(bts, &r) - - return &r, errR + return &r, checkAPIResponse(bts, &r) } -// Delete subtracts ACL from already existing Elasticsearch ACLs config +// Delete removes the specified ACL from the existing ElasticSearch ACLs config. +// +// Deprecated: Use OpenSearchACLConfig.Delete instead. func (conf *ElasticSearchACLConfig) Delete(ctx context.Context, acl ElasticSearchACL) *ElasticSearchACLConfig { - for p, existingAcl := range conf.ACLs { // subtract ALC from existing ACLs config entry that supposed to be deleted + newACLs := []ElasticSearchACL{} // Create a new slice to hold the updated list of ACLs. + + // Iterate over each existing ACL entry. + for _, existingAcl := range conf.ACLs { + // If the ACL usernames match, we'll potentially modify the rules. if acl.Username == existingAcl.Username { - for i := range existingAcl.Rules { - // remove ACL from existing ACLs list - for _, rule := range acl.Rules { - if existingAcl.Rules[i].Permission == rule.Permission && existingAcl.Rules[i].Index == rule.Index { - conf.ACLs[p].Rules = append(conf.ACLs[p].Rules[:i], conf.ACLs[p].Rules[i+1:]...) + newRules := []ElasticsearchACLRule{} // Create a new slice to hold the updated list of rules. + + // Check each existing rule against the rules in the ACL to be deleted. + for _, existingRule := range existingAcl.Rules { + match := false // Flag to track if the existing rule matches any rule in the ACL to be deleted. + for _, ruleToDelete := range acl.Rules { + if existingRule.Permission == ruleToDelete.Permission && existingRule.Index == ruleToDelete.Index { + match = true // The existing rule matches a rule in the ACL to be deleted. + break } } - - // delete ACL item from ACLs list is there are not rules attached to it - if len(conf.ACLs[p].Rules) == 0 { - conf.ACLs = append(conf.ACLs[:p], conf.ACLs[p+1:]...) + // If the existing rule doesn't match any rule in the ACL to be deleted, add it to the new list. + if !match { + newRules = append(newRules, existingRule) } } + + // If there are remaining rules after deletion, add the modified ACL to the new list. + if len(newRules) > 0 { + existingAcl.Rules = newRules + newACLs = append(newACLs, existingAcl) + } + } else { + // If the usernames don't match, directly add the existing ACL to the new list. + newACLs = append(newACLs, existingAcl) } } + // Replace the original list of ACLs with the updated list. + conf.ACLs = newACLs return conf } -// Add appends new ACL to already existing Elasticsearch ACLs config +// Add appends new ACL to the existing ElasticSearch ACLs config. +// +// Deprecated: Use OpenSearchACLConfig.Add instead. func (conf *ElasticSearchACLConfig) Add(acl ElasticSearchACL) *ElasticSearchACLConfig { - var userAlreadyExist bool var userIndex int + userExists := false - // check what ACL rules we already have for a user, and if we find that rule already exists, - // remove it from a rules slice since there is no need of adding duplicates records to the ACL list + // Iterate over the existing ACLs to identify duplicates and determine user existence. for p, existingAcl := range conf.ACLs { - if acl.Username == existingAcl.Username { // ACL record for this user already exists - userAlreadyExist = true + if acl.Username == existingAcl.Username { + userExists = true userIndex = p - for _, existingRule := range existingAcl.Rules { - for i, rule := range acl.Rules { - if existingRule.Permission == rule.Permission && existingRule.Index == rule.Index { - // remove rule since it already exists for this user - acl.Rules = append(acl.Rules[:i], acl.Rules[i+1:]...) + + // Filter out any rules in the ACL to add that already exist for the user. + remainingRules := []ElasticsearchACLRule{} + for _, rule := range acl.Rules { + exists := false + for _, existingRule := range existingAcl.Rules { + if rule.Permission == existingRule.Permission && rule.Index == existingRule.Index { + exists = true + break } } + if !exists { + remainingRules = append(remainingRules, rule) + } } + acl.Rules = remainingRules } } + // If no rules remain for the user, return the existing configuration. if len(acl.Rules) == 0 { - return conf // nothing to add to already existing ACL rules list for a user + return conf } - // add to existing Elasticsearch ACL config new records - if userAlreadyExist { + // Add the new or updated ACL to the config. + if userExists { conf.ACLs[userIndex].Rules = append(conf.ACLs[userIndex].Rules, acl.Rules...) } else { conf.ACLs = append(conf.ACLs, acl) diff --git a/opensearch_acls.go b/opensearch_acls.go new file mode 100644 index 0000000..a84aefb --- /dev/null +++ b/opensearch_acls.go @@ -0,0 +1,151 @@ +package aiven + +import "context" + +type ( + // OpenSearchACLsHandler Aiven go-client handler for OpenSearch ACLs + OpenSearchACLsHandler struct { + client *Client + } + + // OpenSearchACLRequest Aiven API request + // https://api.aiven.io/v1/project//service//opensearch/acl + OpenSearchACLRequest struct { + OpenSearchACLConfig OpenSearchACLConfig `json:"opensearch_acl_config"` + } + + // OpenSearchACLResponse Aiven API response + // https://api.aiven.io/v1/project//service//opensearch/acl + OpenSearchACLResponse struct { + APIResponse + OpenSearchACLConfig OpenSearchACLConfig `json:"opensearch_acl_config"` + } + + // OpenSearchACLConfig represents a configuration for OpenSearch ACLs + OpenSearchACLConfig struct { + ACLs []OpenSearchACL `json:"acls"` + Enabled bool `json:"enabled"` + ExtendedAcl bool `json:"extendedAcl"` + } + + // OpenSearchACL represents a OpenSearch ACLs entry + OpenSearchACL struct { + Rules []OpenSearchACLRule `json:"rules"` + Username string `json:"username"` + } + + // OpenSearchACLRule represents a OpenSearch ACLs Rule entry + OpenSearchACLRule struct { + Index string `json:"index"` + Permission string `json:"permission"` + } +) + +// Update updates OpenSearch ACL config +func (h *OpenSearchACLsHandler) Update(ctx context.Context, project, service string, req OpenSearchACLRequest) (*OpenSearchACLResponse, error) { + path := buildPath("project", project, "service", service, "opensearch", "acl") + bts, err := h.client.doPutRequest(ctx, path, req) + if err != nil { + return nil, err + } + + var r OpenSearchACLResponse + return &r, checkAPIResponse(bts, &r) +} + +// Get gets all existing OpenSearch ACLs config +func (h *OpenSearchACLsHandler) Get(ctx context.Context, project, service string) (*OpenSearchACLResponse, error) { + path := buildPath("project", project, "service", service, "opensearch", "acl") + bts, err := h.client.doGetRequest(ctx, path, nil) + if err != nil { + return nil, err + } + + var r OpenSearchACLResponse + return &r, checkAPIResponse(bts, &r) +} + +// Delete removes the specified ACL from the existing OpenSearch ACLs config. +func (conf *OpenSearchACLConfig) Delete(ctx context.Context, acl OpenSearchACL) *OpenSearchACLConfig { + newACLs := []OpenSearchACL{} // Create a new slice to hold the updated list of ACLs. + + // Iterate over each existing ACL entry. + for _, existingAcl := range conf.ACLs { + // If the ACL usernames match, we'll potentially modify the rules. + if acl.Username == existingAcl.Username { + newRules := []OpenSearchACLRule{} // Create a new slice to hold the updated list of rules. + + // Check each existing rule against the rules in the ACL to be deleted. + for _, existingRule := range existingAcl.Rules { + match := false // Flag to track if the existing rule matches any rule in the ACL to be deleted. + for _, ruleToDelete := range acl.Rules { + if existingRule.Permission == ruleToDelete.Permission && existingRule.Index == ruleToDelete.Index { + match = true // The existing rule matches a rule in the ACL to be deleted. + break + } + } + // If the existing rule doesn't match any rule in the ACL to be deleted, add it to the new list. + if !match { + newRules = append(newRules, existingRule) + } + } + + // If there are remaining rules after deletion, add the modified ACL to the new list. + if len(newRules) > 0 { + existingAcl.Rules = newRules + newACLs = append(newACLs, existingAcl) + } + } else { + // If the usernames don't match, directly add the existing ACL to the new list. + newACLs = append(newACLs, existingAcl) + } + } + + // Replace the original list of ACLs with the updated list. + conf.ACLs = newACLs + return conf +} + +// Add appends new ACL to the existing OpenSearch ACLs config. +func (conf *OpenSearchACLConfig) Add(acl OpenSearchACL) *OpenSearchACLConfig { + var userIndex int + userExists := false + + // Iterate over the existing ACLs to identify duplicates and determine user existence. + for p, existingAcl := range conf.ACLs { + if acl.Username == existingAcl.Username { + userExists = true + userIndex = p + + // Filter out any rules in the ACL to add that already exist for the user. + remainingRules := []OpenSearchACLRule{} + for _, rule := range acl.Rules { + exists := false + for _, existingRule := range existingAcl.Rules { + if rule.Permission == existingRule.Permission && rule.Index == existingRule.Index { + exists = true + break + } + } + if !exists { + remainingRules = append(remainingRules, rule) + } + } + acl.Rules = remainingRules + } + } + + // If no rules remain for the user, return the existing configuration. + if len(acl.Rules) == 0 { + return conf + } + + // Add the new or updated ACL to the config. + if userExists { + conf.ACLs[userIndex].Rules = append(conf.ACLs[userIndex].Rules, acl.Rules...) + } else { + conf.ACLs = append(conf.ACLs, acl) + } + + return conf +} diff --git a/opensearch_acls_test.go b/opensearch_acls_test.go new file mode 100644 index 0000000..d37fc78 --- /dev/null +++ b/opensearch_acls_test.go @@ -0,0 +1,405 @@ +package aiven + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func setupOpenSearchACLsTestCase(t *testing.T) (*Client, func(t *testing.T)) { + t.Log("setup OpenSearchACLs test case") + + const ( + UserName = "test@aiven.io" + UserPassword = "testabcd" + AccessToken = "some-random-token" + ) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/userauth" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(authResponse{ + Token: AccessToken, + State: "active", + }) + + if err != nil { + t.Error(err) + } + return + } + + if r.URL.Path == "/project/test-pr/service/test-sr/opensearch/acl" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(OpenSearchACLResponse{ + OpenSearchACLConfig: OpenSearchACLConfig{ + ACLs: []OpenSearchACL{ + { + Rules: []OpenSearchACLRule{{ + Index: "_all", + Permission: "admin", + }}, + Username: "test-user", + }, + }, + Enabled: true, + ExtendedAcl: false, + }}) + + if err != nil { + t.Error(err) + } + + return + } + })) + + apiUrl = ts.URL + c, err := NewUserClient(UserName, UserPassword, "aiven-go-client-test/"+Version()) + if err != nil { + t.Fatalf("user authentication error: %s", err) + } + + return c, func(t *testing.T) { + t.Log("teardown OpenSearchACLs test case") + ts.Close() + } +} + +func TestOpenSearchACLsHandler_Update(t *testing.T) { + c, tearDown := setupOpenSearchACLsTestCase(t) + defer tearDown(t) + + ctx := context.Background() + + type fields struct { + client *Client + } + type args struct { + project string + service string + req OpenSearchACLRequest + } + tests := []struct { + name string + fields fields + args args + want *OpenSearchACLResponse + wantErr bool + }{ + { + "correct", + fields{client: c}, + args{ + project: "test-pr", + service: "test-sr", + req: OpenSearchACLRequest{ + OpenSearchACLConfig: OpenSearchACLConfig{ + ACLs: []OpenSearchACL{ + { + Rules: []OpenSearchACLRule{{ + Index: "_all", + Permission: "admin", + }}, + Username: "test-user", + }, + }, + Enabled: true, + ExtendedAcl: false, + }, + }, + }, + &OpenSearchACLResponse{ + OpenSearchACLConfig: OpenSearchACLConfig{ + ACLs: []OpenSearchACL{ + { + Rules: []OpenSearchACLRule{{ + Index: "_all", + Permission: "admin", + }}, + Username: "test-user", + }, + }, + Enabled: true, + ExtendedAcl: false, + }, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &OpenSearchACLsHandler{ + client: tt.fields.client, + } + got, err := h.Update(ctx, tt.args.project, tt.args.service, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("Update() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Update() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpenSearchACLsHandler_Get(t *testing.T) { + c, tearDown := setupOpenSearchACLsTestCase(t) + defer tearDown(t) + + ctx := context.Background() + + type fields struct { + client *Client + } + type args struct { + project string + service string + } + tests := []struct { + name string + fields fields + args args + want *OpenSearchACLResponse + wantErr bool + }{ + { + "correct", + fields{client: c}, + args{ + project: "test-pr", + service: "test-sr", + }, + &OpenSearchACLResponse{ + OpenSearchACLConfig: OpenSearchACLConfig{ + ACLs: []OpenSearchACL{ + { + Rules: []OpenSearchACLRule{{ + Index: "_all", + Permission: "admin", + }}, + Username: "test-user", + }, + }, + Enabled: true, + ExtendedAcl: false, + }, + APIResponse: APIResponse{}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &OpenSearchACLsHandler{ + client: tt.fields.client, + } + got, err := h.Get(ctx, tt.args.project, tt.args.service) + if (err != nil) != tt.wantErr { + t.Errorf("List() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("List() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpenSearchACLConfig_Add(t *testing.T) { + type fields struct { + ACLs []OpenSearchACL + Enabled bool + ExtendedAcl bool + } + type args struct { + acl OpenSearchACL + } + tests := []struct { + name string + fields fields + args args + want *OpenSearchACLConfig + }{ + { + "add-multiple", + fields{ + ACLs: []OpenSearchACL{ + { + Username: "test-user", + Rules: []OpenSearchACLRule{ + { + Index: "_rw", + Permission: "write", + }, + }}, + { + Username: "test-user2", + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + }}, + }, + }, + args{acl: OpenSearchACL{ + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + { + Index: "_test", + Permission: "write", + }, + }, + Username: "test-user", + }}, + &OpenSearchACLConfig{ + ACLs: []OpenSearchACL{ + { + Username: "test-user", + Rules: []OpenSearchACLRule{ + { + Index: "_rw", + Permission: "write", + }, + { + Index: "_all", + Permission: "admin", + }, + { + Index: "_test", + Permission: "write", + }, + }}, + { + Username: "test-user2", + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + }}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := OpenSearchACLConfig{ + ACLs: tt.fields.ACLs, + Enabled: tt.fields.Enabled, + ExtendedAcl: tt.fields.ExtendedAcl, + } + if got := conf.Add(tt.args.acl); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Add() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpenSearchACLConfig_Delete(t *testing.T) { + ctx := context.Background() + type fields struct { + ACLs []OpenSearchACL + Enabled bool + ExtendedAcl bool + } + type args struct { + acl OpenSearchACL + } + tests := []struct { + name string + fields fields + args args + want *OpenSearchACLConfig + }{ + { + "multiple", + fields{ + ACLs: []OpenSearchACL{ + { + Username: "test-user", + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + { + Index: "_rw", + Permission: "readwrite", + }, + { + Index: "_test", + Permission: "write", + }, + }}, + { + Username: "test-user2", + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + }}, + }, + Enabled: false, + ExtendedAcl: false, + }, + args{acl: OpenSearchACL{ + Username: "test-user", + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + { + Index: "_rw", + Permission: "readwrite", + }, + }}}, + &OpenSearchACLConfig{ + ACLs: []OpenSearchACL{ + { + Username: "test-user", + Rules: []OpenSearchACLRule{ + { + Index: "_test", + Permission: "write", + }, + }}, + { + Username: "test-user2", + Rules: []OpenSearchACLRule{ + { + Index: "_all", + Permission: "admin", + }, + }}, + }, + Enabled: false, + ExtendedAcl: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := OpenSearchACLConfig{ + ACLs: tt.fields.ACLs, + Enabled: tt.fields.Enabled, + ExtendedAcl: tt.fields.ExtendedAcl, + } + if got := conf.Delete(ctx, tt.args.acl); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Delete() = %v, want %v", got, tt.want) + } + }) + } +}