diff --git a/internal/dao/auditlog.go b/internal/dao/auditlog.go index f5f3d96..d66347e 100644 --- a/internal/dao/auditlog.go +++ b/internal/dao/auditlog.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/paralus/paralus/internal/models" + "github.com/paralus/paralus/pkg/audit" "github.com/paralus/paralus/pkg/query" "github.com/uptrace/bun" ) @@ -19,7 +20,7 @@ func GetAuditLogAggregations(ctx context.Context, db *bun.DB, tag, field string, sq.ColumnExpr("data->>'type' as key"). Where("tag = ?", tag).GroupExpr("data->>'type'") case "username": - if tag != "kubectl_api" { + if tag != audit.KUBECTL_API { sq.ColumnExpr("data->'actor'->'account'->>'username' as key"). Where("tag = ?", tag).GroupExpr("data->'actor'->'account'->>'username'") } else { @@ -45,9 +46,9 @@ func GetAuditLogAggregations(ctx context.Context, db *bun.DB, tag, field string, // add filters switch tag { - case "kubectl_api": + case audit.KUBECTL_API: sq = buildRelayAuditQuery(sq, filters) - case "system", "kubectl_cmd": + case audit.SYSTEM, audit.KUBECTL_CMD: sq = buildQuery(sq, filters) } @@ -61,9 +62,9 @@ func GetAuditLogs(ctx context.Context, db *bun.DB, tag string, filters query.Que Where("tag = ?", tag) switch tag { - case "kubectl_api": + case audit.KUBECTL_API: sq = buildRelayAuditQuery(sq, filters) - case "system", "kubectl_cmd": + case audit.SYSTEM, audit.KUBECTL_CMD: sq = buildQuery(sq, filters) } diff --git a/main.go b/main.go index e5c47e2..ca482f0 100644 --- a/main.go +++ b/main.go @@ -365,9 +365,9 @@ func setup() { gps = service.NewGroupPermissionService(db) switch auditLogStorage { - case "database": + case audit.DATABASE: // audit services - aus, err = service.NewAuditLogDatabaseService(db, "system") + aus, err = service.NewAuditLogDatabaseService(db, audit.SYSTEM) if err != nil { if dev && strings.Contains(err.Error(), "connect: connection refused") { // This is primarily from ES not being available. ES being @@ -379,7 +379,7 @@ func setup() { _log.Fatalw("unable to create auditLog service", "error", err) } } - ras, err = service.NewRelayAuditDatabaseService(db, "kubectl_api") + ras, err = service.NewRelayAuditDatabaseService(db, audit.KUBECTL_API) if err != nil { if dev && strings.Contains(err.Error(), "connect: connection refused") { _log.Warn("unable to create relayAudit service: ", err) @@ -387,7 +387,7 @@ func setup() { _log.Fatalw("unable to create relayAudit service", "error", err) } } - rcs, err = service.NewAuditLogDatabaseService(db, "kubectl_cmd") + rcs, err = service.NewAuditLogDatabaseService(db, audit.KUBECTL_CMD) if err != nil { if dev && strings.Contains(err.Error(), "connect: connection refused") { _log.Warn("unable to create auditLog service:", err) @@ -395,7 +395,7 @@ func setup() { _log.Fatalw("unable to create auditLog service", "error", err) } } - case "elasticsearch": + case audit.ELASTICSEARCH: // audit services aus, err = service.NewAuditLogElasticSearchService(elasticSearchUrl, esIndexPrefix+"-*", "AuditLog API: ") if err != nil { @@ -426,7 +426,7 @@ func setup() { } } default: - + _log.Warn("unable to create audit log service: invalid storage option ! should be either %s or %s", audit.DATABASE, audit.ELASTICSEARCH) } // cluster bootstrap diff --git a/pkg/audit/storage_types.go b/pkg/audit/storage_types.go new file mode 100644 index 0000000..521fca2 --- /dev/null +++ b/pkg/audit/storage_types.go @@ -0,0 +1,12 @@ +package audit + +const ( + DATABASE string = "database" + ELASTICSEARCH string = "elasticsearch" +) + +const ( + SYSTEM string = "system" + KUBECTL_CMD string = "kubectl_cmd" + KUBECTL_API string = "kubectl_api" +) diff --git a/pkg/service/audit_log_db.go b/pkg/service/audit_log_db.go index 2df4716..57e45bf 100644 --- a/pkg/service/audit_log_db.go +++ b/pkg/service/audit_log_db.go @@ -91,8 +91,14 @@ func (a *auditLogDatabaseService) GetAuditLogByProjects(req *v1.GetAuditLogSearc } var resMap map[string]interface{} - data, _ := json.Marshal(response) - json.Unmarshal(data, &resMap) + data, err := json.Marshal(response) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &resMap) + if err != nil { + return nil, err + } result, _ := structpb.NewStruct(resMap) res = &v1.GetAuditLogSearchResponse{ diff --git a/pkg/service/audit_log_db_test.go b/pkg/service/audit_log_db_test.go new file mode 100644 index 0000000..8ac5848 --- /dev/null +++ b/pkg/service/audit_log_db_test.go @@ -0,0 +1,218 @@ +package service + +import ( + "encoding/json" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/paralus/paralus/pkg/audit" + eventv1 "github.com/paralus/paralus/proto/rpc/audit" + auditv1 "github.com/paralus/paralus/proto/types/audit" + commonv3 "github.com/paralus/paralus/proto/types/commonpb/v3" +) + +const project = "projectone" + +func TestAuditLog(t *testing.T) { + + testcases := []struct { + name string + run func(t *testing.T) + }{ + { + name: "auditlog-lasthour", + run: testGetAuditLogForLastHour(audit.SYSTEM), + }, + { + name: "auditlog-lastday", + run: testGetAuditLogForDay(audit.SYSTEM), + }, + { + name: "kubectlcmd-lastday", + run: testGetKubectlCommands(audit.KUBECTL_CMD), + }, + } + + t.Run("auditlogs", func(t *testing.T) { + for _, testcase := range testcases { + tc := testcase + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.run(t) + }) + } + }) +} + +func testGetAuditLogForLastHour(tag string) func(t *testing.T) { + return func(t *testing.T) { + + db, mock := getDB(t) + defer db.Close() + + as, err := NewAuditLogDatabaseService(db, tag) + if err != nil { + t.Fatal(err) + } + + timefrom := "1h" + auditrecord := "{\"actor\":{\"account\":{\"username\":\"admin@paralus.local\"},\"groups\":[\"All Local Users\",\"Organization Admins\"],\"type\":\"USER\"},\"category\":\"AUDIT\",\"client\":{\"host\":\"console-ic-oss.dev.rafay-edge.net\",\"ip\":\"10.0.0.147:47204\",\"type\":\"BROWSER\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36\"},\"detail\":{\"message\":\"Cluster kind-local deleted\",\"meta\":{\"cluster_name\":\"kind-local\"}},\"origin\":\"core\",\"portal\":\"OPS\",\"project\":\"stage\",\"timestamp\":\"2022-11-21T09:48:30.597615647Z\",\"type\":\"cluster.delete.success\",\"version\":\"1.0\"}" + + req := &eventv1.GetAuditLogSearchRequest{ + Metadata: &commonv3.Metadata{ + UrlScope: "auditlogs/" + project, + }, + Filter: &eventv1.AuditLogQueryFilter{ + Timefrom: "now-" + timefrom, + Projects: []string{project}, + }, + } + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "auditlog"."tag", "auditlog"."time", "auditlog"."data" FROM "audit_logs" AS "auditlog" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now())`)). + WillReturnRows(sqlmock.NewRows([]string{"tag", "time", "data"}).AddRow("system", time.Now(), auditrecord)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'project' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'project'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "cluster")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->'actor'->'account'->>'username' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->'actor'->'account'->>'username'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "username")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'type' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'type'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "type")) + + res, err := as.GetAuditLogByProjects(req) + if err != nil { + t.Fatal("could not get audit logs:", err) + } + + var response auditv1.AuditResponse + data, err := json.Marshal(res.Result) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(data, &response) + if err != nil { + t.Fatal(err) + } + + if len(response.GetHits().Hits) != 1 { + t.Fail() + } + } +} + +func testGetAuditLogForDay(tag string) func(t *testing.T) { + return func(t *testing.T) { + + db, mock := getDB(t) + defer db.Close() + + as, err := NewAuditLogDatabaseService(db, tag) + if err != nil { + t.Fatal(err) + } + + timefrom := "1d" + auditrecord := "{\"actor\":{\"account\":{\"username\":\"admin@paralus.local\"},\"groups\":[\"All Local Users\",\"Organization Admins\"],\"type\":\"USER\"},\"category\":\"AUDIT\",\"client\":{\"host\":\"console-ic-oss.dev.rafay-edge.net\",\"ip\":\"10.0.0.147:47204\",\"type\":\"BROWSER\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36\"},\"detail\":{\"message\":\"Cluster kind-local deleted\",\"meta\":{\"cluster_name\":\"kind-local\"}},\"origin\":\"core\",\"portal\":\"OPS\",\"project\":\"stage\",\"timestamp\":\"2022-11-21T09:48:30.597615647Z\",\"type\":\"cluster.delete.success\",\"version\":\"1.0\"}" + auditrecordtwo := "{\"actor\":{\"account\":{\"username\":\"admin@paralus.local\"},\"groups\":[\"All Local Users\",\"Organization Admins\"],\"type\":\"USER\"},\"category\":\"AUDIT\",\"client\":{\"host\":\"console-ic-oss.dev.rafay-edge.net\",\"ip\":\"10.0.0.147:47204\",\"type\":\"BROWSER\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36\"},\"detail\":{\"message\":\"Cluster kind-local deleted\",\"meta\":{\"cluster_name\":\"kind-local\"}},\"origin\":\"core\",\"portal\":\"OPS\",\"project\":\"stage\",\"timestamp\":\"2022-11-21T09:48:30.597615647Z\",\"type\":\"cluster.delete.success\",\"version\":\"1.0\"}" + + req := &eventv1.GetAuditLogSearchRequest{ + Metadata: &commonv3.Metadata{ + UrlScope: "auditlogs/" + project, + }, + Filter: &eventv1.AuditLogQueryFilter{ + Timefrom: "now-" + timefrom, + }, + } + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "auditlog"."tag", "auditlog"."time", "auditlog"."data" FROM "audit_logs" AS "auditlog" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now())`)). + WillReturnRows(sqlmock.NewRows([]string{"tag", "time", "data"}).AddRow(tag, time.Now(), auditrecord).AddRow(tag, time.Now(), auditrecordtwo)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'project' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'project'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "project")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->'actor'->'account'->>'username' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->'actor'->'account'->>'username'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "username")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'type' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'type'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "type")) + + res, err := as.GetAuditLog(req) + if err != nil { + t.Fatal("could not get audit logs:", err) + } + + var response auditv1.AuditResponse + data, err := json.Marshal(res.Result) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(data, &response) + if err != nil { + t.Fatal(err) + } + + if len(response.GetHits().Hits) != 2 { + t.Fail() + } + } +} + +func testGetKubectlCommands(tag string) func(t *testing.T) { + return func(t *testing.T) { + + db, mock := getDB(t) + defer db.Close() + + as, err := NewAuditLogDatabaseService(db, tag) + if err != nil { + t.Fatal(err) + } + + timefrom := "1d" + auditrecord := "{\"actor\":{\"account\":{\"username\":\"admin@paralus.local\"},\"groups\":[\"All Local Users\",\"Organization Admins\"],\"type\":\"USER\"},\"category\":\"AUDIT\",\"client\":{\"host\":\"console-ic-oss.dev.rafay-edge.net\",\"ip\":\"122.50.194.215\",\"type\":\"BROWSER\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36\"},\"detail\":{\"message\":\"kubectl get all\",\"meta\":{\"cluster_name\":\"kind-local\"}},\"origin\":\"cluster\",\"portal\":\"ADMIN\",\"project\":\"stage\",\"timestamp\":\"2022-11-21T09:46:30.854455438Z\",\"type\":\"kubectl.command.detail\",\"version\":\"1.0\"}" + auditrecordtwo := "{\"actor\":{\"account\":{\"username\":\"admin@paralus.local\"},\"groups\":[\"All Local Users\",\"Organization Admins\"],\"type\":\"USER\"},\"category\":\"AUDIT\",\"client\":{\"host\":\"console-ic-oss.dev.rafay-edge.net\",\"ip\":\"122.50.194.215\",\"type\":\"BROWSER\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36\"},\"detail\":{\"message\":\"kubectl get all\",\"meta\":{\"cluster_name\":\"kind-local\"}},\"origin\":\"cluster\",\"portal\":\"ADMIN\",\"project\":\"stage\",\"timestamp\":\"2022-11-21T09:46:30.854455438Z\",\"type\":\"kubectl.command.detail\",\"version\":\"1.0\"}" + + req := &eventv1.GetAuditLogSearchRequest{ + Metadata: &commonv3.Metadata{ + UrlScope: "auditlogs/" + project, + }, + Filter: &eventv1.AuditLogQueryFilter{ + Timefrom: "now-" + timefrom, + }, + } + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "auditlog"."tag", "auditlog"."time", "auditlog"."data" FROM "audit_logs" AS "auditlog" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now())`)). + WillReturnRows(sqlmock.NewRows([]string{"tag", "time", "data"}).AddRow(tag, time.Now(), auditrecord).AddRow(tag, time.Now(), auditrecordtwo)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'project' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'project'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "project")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->'actor'->'account'->>'username' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->'actor'->'account'->>'username'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "username")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'type' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'project' = '` + project + `') AND (to_timestamp(data->>'timestamp', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'type'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "type")) + + res, err := as.GetAuditLog(req) + if err != nil { + t.Fatal("could not get audit logs:", err) + } + + var response auditv1.AuditResponse + data, err := json.Marshal(res.Result) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(data, &response) + if err != nil { + t.Fatal(err) + } + + if len(response.GetHits().Hits) != 2 { + t.Fail() + } + } +} diff --git a/pkg/service/relay_audit_db.go b/pkg/service/relay_audit_db.go index e9ef989..f22713e 100644 --- a/pkg/service/relay_audit_db.go +++ b/pkg/service/relay_audit_db.go @@ -88,7 +88,10 @@ func (ra *relayAuditDatabaseService) GetRelayAuditByProjects(req *v1.RelayAuditR } var resMap map[string]interface{} - data, _ := json.Marshal(response) + data, err := json.Marshal(response) + if err != nil { + return nil, err + } json.Unmarshal(data, &resMap) result, _ := structpb.NewStruct(resMap) diff --git a/pkg/service/relay_audit_db_test.go b/pkg/service/relay_audit_db_test.go new file mode 100644 index 0000000..1db047b --- /dev/null +++ b/pkg/service/relay_audit_db_test.go @@ -0,0 +1,168 @@ +package service + +import ( + "encoding/json" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/paralus/paralus/pkg/audit" + eventv1 "github.com/paralus/paralus/proto/rpc/audit" + auditv1 "github.com/paralus/paralus/proto/types/audit" + commonv3 "github.com/paralus/paralus/proto/types/commonpb/v3" +) + +func TestRelayAuditLog(t *testing.T) { + + testcases := []struct { + name string + run func(t *testing.T) + }{ + { + name: "auditlog-kubectlapi-kind", + run: testGetRelayAuditLogForKind(audit.KUBECTL_API), + }, + { + name: "auditlog-kubectlapi-cluster", + run: testGetRelayAuditLogForCluster(audit.KUBECTL_API), + }, + } + + t.Run("relayauditlogs", func(t *testing.T) { + for _, testcase := range testcases { + tc := testcase + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.run(t) + }) + } + }) +} + +func testGetRelayAuditLogForCluster(tag string) func(t *testing.T) { + return func(t *testing.T) { + + db, mock := getDB(t) + defer db.Close() + + ras, err := NewRelayAuditDatabaseService(db, tag) + if err != nil { + t.Fatal(err) + } + + timefrom := "1h" + auditrecord := "{\"av\":\"flowcontrol.apiserver.k8s.io/v1beta2\",\"cn\":\"kind-2\",\"d\": 0.492472386,\"id\":\"cduajnic6p9tna60re3g\",\"k\":\"\",\"m\": \"GET\", \"n\": \"\", \"ns\": \"\", \"o\": \"9fcdf482-6191-44f1-987a-8469addf2566\", \"p\": \"ba184458-b899-4cf3-99fa-d77a21578ede\", \"q\": \"timeout=32s\", \"ra\": \"10.0.0.147\", \"sc\": 200, \"st\": \"browser shell\", \"ts\": \"2022-11-22T10:52:14.987Z\", \"un\": \"admin@paralus.local\", \"url\": \"/apis/flowcontrol.apiserver.k8s.io/v1beta2\", \"w\": 819 }\"" + + req := &eventv1.RelayAuditRequest{ + Metadata: &commonv3.Metadata{ + UrlScope: "auditlogs/" + project, + }, + Filter: &eventv1.RelayAuditQueryFilter{ + Cluster: "kind-2", + Timefrom: "now-" + timefrom, + }, + } + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "auditlog"."tag", "auditlog"."time", "auditlog"."data" FROM "audit_logs" AS "auditlog" WHERE (tag = '` + tag + `') AND (data->>'cn' = '` + req.Filter.Cluster + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now())`)). + WillReturnRows(sqlmock.NewRows([]string{"tag", "time", "data"}).AddRow(tag, time.Now(), auditrecord)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'cn' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'cn' = '` + req.Filter.Cluster + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'cn'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "cluster")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'un' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'cn' = '` + req.Filter.Cluster + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'un'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "username")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'n' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'cn' = '` + req.Filter.Cluster + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'n'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "namespace")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'k' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'cn' = '` + req.Filter.Cluster + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'k'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "kind")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'m' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'cn' = '` + req.Filter.Cluster + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'m'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "method")) + + res, err := ras.GetRelayAudit(req) + if err != nil { + t.Fatal("could not get audit logs:", err) + } + + var response auditv1.AuditResponse + data, err := json.Marshal(res.Result) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(data, &response) + if err != nil { + t.Fatal(err) + } + + if len(response.GetHits().Hits) != 1 { + t.Fail() + } + } +} + +func testGetRelayAuditLogForKind(tag string) func(t *testing.T) { + return func(t *testing.T) { + + db, mock := getDB(t) + defer db.Close() + + as, err := NewRelayAuditDatabaseService(db, tag) + if err != nil { + t.Fatal(err) + } + + timefrom := "1d" + auditrecord := "{\"av\":\"flowcontrol.apiserver.k8s.io/v1beta2\",\"cn\":\"kind-2\",\"d\": 0.492472386,\"id\":\"cduajnic6p9tna60re3g\",\"k\":\"namespace\",\"m\": \"GET\", \"n\": \"\", \"ns\": \"\", \"o\": \"9fcdf482-6191-44f1-987a-8469addf2566\", \"p\": \"ba184458-b899-4cf3-99fa-d77a21578ede\", \"q\": \"timeout=32s\", \"ra\": \"10.0.0.147\", \"sc\": 200, \"st\": \"browser shell\", \"ts\": \"2022-11-22T10:52:14.987Z\", \"un\": \"admin@paralus.local\", \"url\": \"/apis/flowcontrol.apiserver.k8s.io/v1beta2\", \"w\": 819 }\"" + auditrecordtwo := "{\"av\":\"flowcontrol.apiserver.k8s.io/v1beta2\",\"cn\":\"kind-2\",\"d\": 0.492472386,\"id\":\"cduajnic6p9tna60re3g\",\"k\":\"namespace\",\"m\": \"GET\", \"n\": \"\", \"ns\": \"\", \"o\": \"9fcdf482-6191-44f1-987a-8469addf2566\", \"p\": \"ba184458-b899-4cf3-99fa-d77a21578ede\", \"q\": \"timeout=32s\", \"ra\": \"10.0.0.147\", \"sc\": 200, \"st\": \"browser shell\", \"ts\": \"2022-11-22T10:52:14.987Z\", \"un\": \"admin@paralus.local\", \"url\": \"/apis/flowcontrol.apiserver.k8s.io/v1beta2\", \"w\": 819 }\"" + + req := &eventv1.RelayAuditRequest{ + Metadata: &commonv3.Metadata{ + UrlScope: "auditlogs/" + project, + }, + Filter: &eventv1.RelayAuditQueryFilter{ + Kind: "namespace", + Timefrom: "now-" + timefrom, + }, + } + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "auditlog"."tag", "auditlog"."time", "auditlog"."data" FROM "audit_logs" AS "auditlog" WHERE (tag = '` + tag + `') AND (data->>'k' = '` + req.Filter.Kind + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now())`)). + WillReturnRows(sqlmock.NewRows([]string{"tag", "time", "data"}).AddRow(tag, time.Now(), auditrecord).AddRow(tag, time.Now(), auditrecordtwo)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'cn' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'k' = '` + req.Filter.Kind + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'cn'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "cluster")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'un' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'k' = '` + req.Filter.Kind + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'un'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "username")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'n' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'k' = '` + req.Filter.Kind + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'n'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "namespace")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'k' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'k' = '` + req.Filter.Kind + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'k'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "kind")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(1) as count, data->>'m' as key FROM "audit_logs" WHERE (tag = '` + tag + `') AND (data->>'k' = '` + req.Filter.Kind + `') AND (to_timestamp(data->>'ts', 'YYYY-MM-DD"T"HH:MI:SS') between now() - interval '` + timefrom + `' and now()) GROUP BY data->>'m'`)). + WillReturnRows(sqlmock.NewRows([]string{"count", "key"}).AddRow(1, "method")) + + res, err := as.GetRelayAudit(req) + if err != nil { + t.Fatal("could not get audit logs:", err) + } + + var response auditv1.AuditResponse + data, err := json.Marshal(res.Result) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(data, &response) + if err != nil { + t.Fatal(err) + } + + if len(response.GetHits().Hits) != 2 { + t.Fail() + } + } +}