diff --git a/internal/dao/namespaces.go b/internal/dao/namespaces.go index 0147e8e..0a3e88b 100644 --- a/internal/dao/namespaces.go +++ b/internal/dao/namespaces.go @@ -14,7 +14,7 @@ func GetProjectNamespaces(ctx context.Context, db bun.IDB, projectID uuid.UUID) var panr []models.ProjectAccountNamespaceRole err := db.NewSelect().Model(&panr).Where("project_id = ?", projectID).Where("trash = ?", false).Scan(ctx) - if err != sql.ErrNoRows { + if err != nil && err != sql.ErrNoRows { return nil, err } for _, nr := range panr { @@ -23,7 +23,7 @@ func GetProjectNamespaces(ctx context.Context, db bun.IDB, projectID uuid.UUID) var pgnr []models.ProjectGroupNamespaceRole err = db.NewSelect().Model(&pgnr).Where("project_id = ?", projectID).Where("trash = ?", false).Scan(ctx) - if err != sql.ErrNoRows { + if err != nil && err != sql.ErrNoRows { return nil, err } for _, nr := range pgnr { diff --git a/pkg/service/apikey_test.go b/pkg/service/apikey_test.go new file mode 100644 index 0000000..0fe9a91 --- /dev/null +++ b/pkg/service/apikey_test.go @@ -0,0 +1,122 @@ +package service + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/google/uuid" + userrpcv3 "github.com/paralus/paralus/proto/rpc/user" +) + +func TestApiKeyCreate(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ak := NewApiKeyService(db, getLogger()) + uuuid := uuid.NewString() + auuid := uuid.NewString() + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + + // mocks + mock.ExpectQuery(`INSERT INTO "authsrv_apikey"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(auuid, "apikey-"+auuid)) + + resp, err := ak.Create(context.Background(), req) + if err != nil { + t.Error("unable to create apikey:", err) + } + if resp.ID != uuid.MustParse(auuid) { + t.Errorf("incorrect id for apikey; expected '%v', got '%v'", uuid.MustParse(auuid), resp.ID) + } + if resp.Name != "apikey-"+auuid { + t.Errorf("incorrect name for apikey; expected '%v', got '%v'", "apikey-"+auuid, resp.Name) + } +} + +func TestApiKeyDelete(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ak := NewApiKeyService(db, getLogger()) + uuuid := uuid.NewString() + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + + // mocks + mock.ExpectExec(`UPDATE "authsrv_apikey" AS "apikey" SET trash = TRUE WHERE \(account_id = 'user-` + uuuid + `'\) AND \(key = '` + uuuid + `'\)`). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _, err := ak.Delete(context.Background(), req) + if err != nil { + t.Error("unable to delete apikey:", err) + } +} + +func TestApiKeyGet(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ak := NewApiKeyService(db, getLogger()) + uuuid := uuid.NewString() + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + + // mocks + mock.ExpectQuery(`SELECT "apikey"."id", "apikey"."name", .*FROM "authsrv_apikey" AS "apikey" WHERE \(name = 'user-` + uuuid + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uuuid, "user-"+uuuid)) + + resp, err := ak.Get(context.Background(), req) + if err != nil { + t.Error("unable to get apikey:", err) + } + if resp.ID != uuid.MustParse(uuuid) { + t.Errorf("incorrect id for apikey; expected '%v', got '%v'", uuid.MustParse(uuuid), resp.ID) + } + if resp.Name != "user-"+uuuid { + t.Errorf("incorrect name for apikey; expected '%v', got '%v'", "apikey-"+uuuid, resp.Name) + } +} + +func TestApiKeyGetByKey(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ak := NewApiKeyService(db, getLogger()) + uuuid := uuid.NewString() + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + + // mocks + mock.ExpectQuery(`SELECT "apikey"."id", "apikey"."name", .*FROM "authsrv_apikey" AS "apikey" WHERE \(key = '` + uuuid + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uuuid, "user-"+uuuid)) + + resp, err := ak.GetByKey(context.Background(), req) + if err != nil { + t.Error("unable to get apikey:", err) + } + if resp.ID != uuid.MustParse(uuuid) { + t.Errorf("incorrect id for apikey; expected '%v', got '%v'", uuid.MustParse(uuuid), resp.ID) + } + if resp.Name != "user-"+uuuid { + t.Errorf("incorrect name for apikey; expected '%v', got '%v'", "apikey-"+uuuid, resp.Name) + } +} + +func TestApiKeyList(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ak := NewApiKeyService(db, getLogger()) + uuuid := uuid.NewString() + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + + // mocks + mock.ExpectQuery(`SELECT "apikey"."id", "apikey"."name", .*FROM "authsrv_apikey" AS "apikey" WHERE \(account_id = 'user-` + uuuid + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uuuid, "user-"+uuuid)) + + resp, err := ak.List(context.Background(), req) + if err != nil { + t.Error("unable to list apikey:", err) + } + if resp.Items[0].Name != "user-"+uuuid { + t.Errorf("incorrect name for apikey; expected '%v', got '%v'", "apikey-"+uuuid, resp.Items[0].Name) + } +} diff --git a/pkg/service/audit_log_query.go b/pkg/service/audit_log_query.go index 68c5f72..344a4c9 100644 --- a/pkg/service/audit_log_query.go +++ b/pkg/service/audit_log_query.go @@ -11,7 +11,7 @@ import ( ) type AuditLogService struct { - auditQuery *ElasticSearchQuery + auditQuery ElasticSearchQuery } func NewAuditLogService(url string, auditPattern string, logPrefix string) (*AuditLogService, error) { diff --git a/pkg/service/audit_log_query_test.go b/pkg/service/audit_log_query_test.go new file mode 100644 index 0000000..abb8971 --- /dev/null +++ b/pkg/service/audit_log_query_test.go @@ -0,0 +1,145 @@ +package service + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + v1 "github.com/paralus/paralus/proto/rpc/audit" + v3 "github.com/paralus/paralus/proto/types/commonpb/v3" +) + +type md struct { + Source []string `json:"_source"` + Aggs struct { + GroupByProject struct { + Aggs struct { + GroupByType struct { + Terms struct { + Field string `json:"field"` + Size int `json:"size"` + } `json:"terms"` + } `json:"group_by_type"` + GroupByUsername struct { + Terms struct { + Field string `json:"field"` + Size int `json:"size"` + } `json:"terms"` + } `json:"group_by_username"` + } `json:"aggs"` + Terms struct { + Field string `json:"field"` + Size int `json:"size"` + } `json:"terms"` + } `json:"group_by_project"` + GroupByType struct { + Terms struct { + Field string `json:"field"` + } `json:"terms"` + } `json:"group_by_type"` + GroupByUsername struct { + Terms struct { + Field string `json:"field"` + } `json:"terms"` + } `json:"group_by_username"` + } `json:"aggs"` + Query struct { + Bool struct { + Filter struct { + Range struct { + JSONTimestamp struct { + Gte string `json:"gte"` + Lt string `json:"lt"` + } `json:"json.timestamp"` + } `json:"range"` + } `json:"filter"` + Must []struct { + Term struct { + JSONCategory string `json:"json.category"` + } `json:"term,omitempty"` + Terms struct { + JSONProject []string `json:"json.project"` + } `json:"terms,omitempty"` + QueryString struct { + Query string `json:"query"` + } `json:"query_string,omitempty"` + } `json:"must"` + } `json:"bool"` + } `json:"query"` + Size int `json:"size"` + Sort struct { + JSONTimestamp struct { + Order string `json:"order"` + } `json:"json.timestamp"` + } `json:"sort"` +} + +type mockElasticSearchQuery struct { + msg []bytes.Buffer +} + +func (m *mockElasticSearchQuery) Handle(msg bytes.Buffer) (map[string]interface{}, error) { + m.msg = append(m.msg, msg) + return map[string]interface{}{}, nil +} + +func TestGetAuditLogByProjectsSimple(t *testing.T) { + esq := &mockElasticSearchQuery{} + al := &AuditLogService{auditQuery: esq} + req := v1.AuditLogSearchRequest{ + Filter: &v1.AuditLogQueryFilter{ + QueryString: "query-string", + Projects: []string{"project-one", "project-two"}, + Timefrom: "now-1h", + Type: "fake-type", + User: "fake-user", + Client: "fake-client", + DashboardData: true, + }, + } + _, err := al.GetAuditLogByProjects(&req) + if err != nil { + t.Error("unable to get audit logs") + } + if len(esq.msg) != 1 { + t.Fatalf("incorrect number of searches; expected '%v', got '%v'", 1, len(esq.msg)) + } + m := &md{} + err = json.Unmarshal(esq.msg[0].Bytes(), m) + if err != nil { + t.Fatal("unable to unmarshall es request") + } + expected := `{"_source":["json"],"aggs":{"group_by_project":{"aggs":{"group_by_type":{"terms":{"field":"json.type","size":1000}},"group_by_username":{"terms":{"field":"json.actor.account.username","size":1000}}},"terms":{"field":"json.project","size":1000}},"group_by_type":{"terms":{"field":"json.type"}},"group_by_username":{"terms":{"field":"json.actor.account.username"}}},"query":{"bool":{"filter":{"range":{"json.timestamp":{"gte":"now-1h","lt":"now"}}},"must":[{"term":{"json.category":"AUDIT"}},{"term":{"json.type":"fake-type"}},{"term":{"json.actor.account.username":"fake-user"}},{"term":{"json.client.type":"fake-client"}},{"terms":{"json.project":["project-one","project-two"]}},{"query_string":{"query":"query-string"}}]}},"size":0,"sort":{"json.timestamp":{"order":"desc"}}}` + if strings.TrimSpace(esq.msg[0].String()) != expected { + t.Errorf("incorrect es query; expected '%v', got '%v'", expected, strings.TrimSpace(esq.msg[0].String())) + } +} + +func TestGetAuditLogByProjectsNoProject(t *testing.T) { + esq := &mockElasticSearchQuery{} + al := &AuditLogService{auditQuery: esq} + req := v1.AuditLogSearchRequest{ + Metadata: &v3.Metadata{UrlScope: "url/project"}, + Filter: &v1.AuditLogQueryFilter{ + QueryString: "query-string", + }, + } + _, err := al.GetAuditLog(&req) + if err != nil { + t.Error("unable to get audit logs", err) + } + if len(esq.msg) != 1 { + t.Fatalf("incorrect number of searches; expected '%v', got '%v'", 1, len(esq.msg)) + } + m := &md{} + err = json.Unmarshal(esq.msg[0].Bytes(), m) + if err != nil { + t.Fatal("unable to unmarshall es request") + } + + expected := `{"_source":["json"],"aggs":{"group_by_type":{"terms":{"field":"json.type"}},"group_by_username":{"terms":{"field":"json.actor.account.username"}}},"query":{"bool":{"must":[{"term":{"json.category":"AUDIT"}},{"terms":{"json.project":["project"]}},{"query_string":{"query":"query-string"}}]}},"size":500,"sort":{"json.timestamp":{"order":"desc"}}}` + if strings.TrimSpace(esq.msg[0].String()) != expected { + t.Errorf("incorrect es query; expected '%v', got '%v'", expected, strings.TrimSpace(esq.msg[0].String())) + } +} diff --git a/pkg/service/elastic_search_query.go b/pkg/service/elastic_search_query.go index 7b59232..660f1fb 100644 --- a/pkg/service/elastic_search_query.go +++ b/pkg/service/elastic_search_query.go @@ -9,14 +9,18 @@ import ( v6Client "github.com/elastic/go-elasticsearch" ) -type ElasticSearchQuery struct { +type elasticSearchQuery struct { url string indexPattern string logPrefix string esClient *v6Client.Client } -func NewElasticSearchQuery(url string, indexPattern string, logPrefix string) (*ElasticSearchQuery, error) { +type ElasticSearchQuery interface { + Handle(bytes.Buffer) (map[string]interface{}, error) +} + +func NewElasticSearchQuery(url string, indexPattern string, logPrefix string) (ElasticSearchQuery, error) { cfg := v6Client.Config{ Addresses: []string{ url, @@ -33,7 +37,7 @@ func NewElasticSearchQuery(url string, indexPattern string, logPrefix string) (* // return nil, err // } // _log.Infow(logPrefix+":Connected to elastic search ", "cluster", res, "index", indexPattern) - esQuery := &ElasticSearchQuery{ + esQuery := &elasticSearchQuery{ url: url, indexPattern: indexPattern, logPrefix: logPrefix, @@ -43,7 +47,7 @@ func NewElasticSearchQuery(url string, indexPattern string, logPrefix string) (* } //Handle Fires the search query -func (q *ElasticSearchQuery) Handle(msg bytes.Buffer) (map[string]interface{}, error) { +func (q *elasticSearchQuery) Handle(msg bytes.Buffer) (map[string]interface{}, error) { _log.Debugw("Searching elastic search: ", "index", q.indexPattern, "url", q.url, "q", q) res, err := q.esClient.Search( q.esClient.Search.WithContext(context.Background()), diff --git a/pkg/service/namespace_test.go b/pkg/service/namespace_test.go new file mode 100644 index 0000000..772e78f --- /dev/null +++ b/pkg/service/namespace_test.go @@ -0,0 +1,79 @@ +package service + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/google/uuid" +) + +func TestGetProjectNamespaces(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + puuid := uuid.New() + mock.ExpectQuery(`SELECT "projectaccountnamespacerole"."id", "projectaccountnamespacerole"."name", "projectaccountnamespacerole"."description", "projectaccountnamespacerole"."created_at", "projectaccountnamespacerole"."modified_at", "projectaccountnamespacerole"."trash", "projectaccountnamespacerole"."organization_id", "projectaccountnamespacerole"."partner_id", "projectaccountnamespacerole"."role_id", "projectaccountnamespacerole"."account_id", "projectaccountnamespacerole"."project_id", "projectaccountnamespacerole"."namespace", "projectaccountnamespacerole"."active" FROM "authsrv_projectaccountnamespacerole" AS "projectaccountnamespacerole" WHERE \(project_id = '` + puuid.String() + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"namespace"}).AddRow("namespace1")) + mock.ExpectQuery(`SELECT "projectgroupnamespacerole"."id", "projectgroupnamespacerole"."name", "projectgroupnamespacerole"."description", "projectgroupnamespacerole"."created_at", "projectgroupnamespacerole"."modified_at", "projectgroupnamespacerole"."trash", "projectgroupnamespacerole"."organization_id", "projectgroupnamespacerole"."partner_id", "projectgroupnamespacerole"."role_id", "projectgroupnamespacerole"."group_id", "projectgroupnamespacerole"."project_id", "projectgroupnamespacerole"."namespace", "projectgroupnamespacerole"."active" FROM "authsrv_projectgroupnamespacerole" AS "projectgroupnamespacerole" WHERE \(project_id = '` + puuid.String() + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"namespace"}).AddRow("namespace2")) + + ns := NewNamespaceService(db) + nl, err := ns.GetProjectNamespaces(context.Background(), puuid) + if err != nil { + t.Fatal("unable to get namespaces", err) + } + if len(nl) != 2 { + t.Errorf("incorrect number of namespaces; expected '%v', got '%v'", 2, len(nl)) + } + if nl[0] != "namespace1" { + t.Errorf("incorrect namespace name; expected '%v', got '%v'", "namespace1", nl[0]) + } + if nl[1] != "namespace2" { + t.Errorf("incorrect namespace name; expected '%v', got '%v'", "namespace2", nl[1]) + } +} + +func TestGetAccountProjectNamespaces(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + puuid := uuid.New() + uuuid := uuid.New() + mock.ExpectQuery(`SELECT "projectaccountnamespacerole"."id", "projectaccountnamespacerole"."name", "projectaccountnamespacerole"."description", "projectaccountnamespacerole"."created_at", "projectaccountnamespacerole"."modified_at", "projectaccountnamespacerole"."trash", "projectaccountnamespacerole"."organization_id", "projectaccountnamespacerole"."partner_id", "projectaccountnamespacerole"."role_id", "projectaccountnamespacerole"."account_id", "projectaccountnamespacerole"."project_id", "projectaccountnamespacerole"."namespace", "projectaccountnamespacerole"."active" FROM "authsrv_projectaccountnamespacerole" AS "projectaccountnamespacerole" WHERE \(project_id = '` + puuid.String() + `'\) AND \(account_id = '` + uuuid.String() + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"namespace"}).AddRow("namespace1")) + + ns := NewNamespaceService(db) + nl, err := ns.GetAccountProjectNamespaces(context.Background(), puuid, uuuid) + if err != nil { + t.Fatal("unable to get namespaces", err) + } + if len(nl) != 1 { + t.Errorf("incorrect number of namespaces; expected '%v', got '%v'", 1, len(nl)) + } + if nl[0] != "namespace1" { + t.Errorf("incorrect namespace name; expected '%v', got '%v'", "namespace1", nl[0]) + } +} + +func TestGetGroupProjectNamespaces(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + puuid := uuid.New() + uuuid := uuid.New() + mock.ExpectQuery(`SELECT "projectgroupnamespacerole"."id", "projectgroupnamespacerole"."name", "projectgroupnamespacerole"."description", "projectgroupnamespacerole"."created_at", "projectgroupnamespacerole"."modified_at", "projectgroupnamespacerole"."trash", "projectgroupnamespacerole"."organization_id", "projectgroupnamespacerole"."partner_id", "projectgroupnamespacerole"."role_id", "projectgroupnamespacerole"."group_id", "projectgroupnamespacerole"."project_id", "projectgroupnamespacerole"."namespace", "projectgroupnamespacerole"."active" FROM "authsrv_projectgroupnamespacerole" AS "projectgroupnamespacerole" JOIN authsrv_groupaccount ON projectgroupnamespacerole.group_id=authsrv_groupaccount.group_id WHERE \(project_id = '+` + puuid.String() + `+'\) AND \(authsrv_groupaccount.account_id = '` + uuuid.String() + `'\) AND \(projectgroupnamespacerole.trash = FALSE\) AND \(authsrv_groupaccount.trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"namespace"}).AddRow("namespace1")) + + ns := NewNamespaceService(db) + nl, err := ns.GetGroupProjectNamespaces(context.Background(), puuid, uuuid) + if err != nil { + t.Fatal("unable to get namespaces", err) + } + if len(nl) != 1 { + t.Errorf("incorrect number of namespaces; expected '%v', got '%v'", 1, len(nl)) + } + if nl[0] != "namespace1" { + t.Errorf("incorrect namespace name; expected '%v', got '%v'", "namespace1", nl[0]) + } +} diff --git a/pkg/service/project_test.go b/pkg/service/project_test.go index 0fe8873..5b569c6 100644 --- a/pkg/service/project_test.go +++ b/pkg/service/project_test.go @@ -7,8 +7,10 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/google/uuid" + "github.com/paralus/paralus/pkg/common" v3 "github.com/paralus/paralus/proto/types/commonpb/v3" systemv3 "github.com/paralus/paralus/proto/types/systempb/v3" + userv3 "github.com/paralus/paralus/proto/types/userpb/v3" ) func performProjectBasicChecks(t *testing.T, project *systemv3.Project, puuid string) { @@ -25,10 +27,8 @@ func TestCreateProject(t *testing.T) { ps := NewProjectService(db, &mazc, getLogger(), true) puuid := uuid.New().String() - ouuid := uuid.New().String() - mock.ExpectQuery(`SELECT "organization"."id", "organization"."name"`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(ouuid)) + addFetchExpectation(mock, "organization") mock.ExpectBegin() mock.ExpectQuery(`INSERT INTO "authsrv_project"`). WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(puuid)) @@ -68,6 +68,52 @@ func TestCreateProjectDuplicate(t *testing.T) { } } +func TestCreateProjectFull(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + mazc := mockAuthzClient{} + ps := NewProjectService(db, &mazc, getLogger(), true) + + puuid := uuid.New().String() + + addFetchExpectation(mock, "organization") + addFetchEmptyExpecteation(mock, "project") // not existing project + mock.ExpectBegin() + mock.ExpectQuery(`INSERT INTO "authsrv_project"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(puuid)) + mock.ExpectQuery(`SELECT "resourcerole"."id", "resourcerole"."name", "resourcerole"."description", "resourcerole"."created_at", "resourcerole"."modified_at", "resourcerole"."trash", "resourcerole"."organization_id", "resourcerole"."partner_id", "resourcerole"."is_global", "resourcerole"."builtin", "resourcerole"."scope" FROM "authsrv_resourcerole" AS "resourcerole" WHERE \(name = 'test-role'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(puuid, "resourcerole-"+puuid, "namespace")) + addFetchExpectation(mock, "group") + mock.ExpectQuery(`INSERT INTO "authsrv_projectgroupnamespacerole"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.NewString())) + mock.ExpectQuery(`SELECT "identities"."id".* FROM "identities" WHERE .*traits ->> 'email' = 'test-user'`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "traits"}).AddRow(uuid.NewString(), []byte(`{"email":"test-user", "first_name": "John", "last_name": "Doe", "description": "The OG user."}`))) + mock.ExpectQuery(`SELECT "resourcerole"."id", "resourcerole"."name", "resourcerole"."description", "resourcerole"."created_at", "resourcerole"."modified_at", "resourcerole"."trash", "resourcerole"."organization_id", "resourcerole"."partner_id", "resourcerole"."is_global", "resourcerole"."builtin", "resourcerole"."scope" FROM "authsrv_resourcerole" AS "resourcerole" WHERE \(name = 'test-role'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name", "scope"}).AddRow(puuid, "resourcerole-"+puuid, "namespace")) + mock.ExpectQuery(`INSERT INTO "authsrv_projectaccountnamespacerole"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.NewString())) + mock.ExpectCommit() + + te := []string{"test-project", "test-namespace", "test-group", "test-role", "test-user"} + project := &systemv3.Project{ + Metadata: &v3.Metadata{Id: puuid, Name: "project-" + puuid, Organization: "orgname"}, + Spec: &systemv3.ProjectSpec{ + ProjectNamespaceRoles: []*userv3.ProjectNamespaceRole{ + {Project: &te[0], Namespace: &te[1], Group: &te[2], Role: te[3]}, + }, + UserRoles: []*userv3.UserRole{ + {User: te[4], Role: te[3]}, + }, + }, + } + project, err := ps.Create(context.Background(), project) + if err != nil { + t.Fatal("could not create project:", err) + } + performProjectBasicChecks(t, project, puuid) +} + func TestProjectDelete(t *testing.T) { db, mock := getDB(t) defer db.Close() @@ -109,8 +155,7 @@ func TestProjectDeleteNonExist(t *testing.T) { puuid := uuid.New().String() - mock.ExpectQuery(`SELECT "project"."id", "project"."name", .* FROM "authsrv_project" AS "project" WHERE`). - WithArgs().WillReturnError(fmt.Errorf("no data available")) + addFailingFetchExpecteation(mock, "project") project := &systemv3.Project{ Metadata: &v3.Metadata{Id: puuid, Name: "project-" + puuid}, @@ -128,18 +173,10 @@ func TestProjectGetByName(t *testing.T) { mazc := mockAuthzClient{} ps := NewProjectService(db, &mazc, getLogger(), true) - partuuid := uuid.New().String() - ouuid := uuid.New().String() puuid := uuid.New().String() - - mock.ExpectQuery(`SELECT "project"."id", "project"."name"`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "organization_id", "partner_id"}).AddRow(puuid, ouuid, partuuid)) - - mock.ExpectQuery(`SELECT "organization"."id", "organization"."name"`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(ouuid)) - - mock.ExpectQuery(`SELECT "partner"."id", "partner"."name"`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(partuuid)) + addFetchExpectation(mock, "project") + addFetchExpectation(mock, "organization") + addFetchExpectation(mock, "partner") mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, authsrv_project.name as project, authsrv_group.name as group FROM "authsrv_projectgrouprole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectgrouprole.role_id @@ -176,10 +213,9 @@ func TestProjectGetById(t *testing.T) { mazc := mockAuthzClient{} ps := NewProjectService(db, &mazc, getLogger(), true) - puuid := uuid.New().String() + puuid := uuid.NewString() - mock.ExpectQuery(`SELECT "project"."id", "project"."name", .* FROM "authsrv_project" AS "project" WHERE .*id = '` + puuid + `'`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(puuid, "project-"+puuid)) + addFetchByIdExpectation(mock, "project", puuid) project := &systemv3.Project{ Metadata: &v3.Metadata{Id: puuid, Name: "project-" + puuid}, @@ -198,10 +234,7 @@ func TestProjectUpdate(t *testing.T) { mazc := mockAuthzClient{} ps := NewProjectService(db, &mazc, getLogger(), true) - puuid := uuid.New().String() - - mock.ExpectQuery(`SELECT "project"."id", "project"."name", .* FROM "authsrv_project" AS "project" WHERE`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(puuid, "project-"+puuid)) + puuid := addFetchExpectation(mock, "project") mock.ExpectBegin() mock.ExpectExec(`UPDATE "authsrv_projectgrouprole" AS "projectgrouprole" SET trash = TRUE WHERE`).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectQuery(`UPDATE "authsrv_projectgroupnamespacerole" AS "projectgroupnamespacerole" SET trash = TRUE WHERE ."project_id" = '` + puuid + `'. AND .trash = false. RETURNING *`). @@ -237,3 +270,115 @@ func TestProjectUpdate(t *testing.T) { t.Fatal("could not update project:", err) } } + +func TestProjectList(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + mazc := mockAuthzClient{} + ps := NewProjectService(db, &mazc, getLogger(), true) + + uid := uuid.NewString() + ouuid := addFetchExpectation(mock, "organization") + puuid := addFetchExpectation(mock, "partner") + mock.ExpectQuery(`SELECT "project"."id", "project"."name", .* FROM "authsrv_project" AS "project" WHERE \(partner_id = '` + puuid + `'\) AND \(organization_id = '` + ouuid + `'\) AND \(trash = false\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uid, "project-"+uid)) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, authsrv_project.name as project, authsrv_group.name as group FROM "authsrv_projectgrouprole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectgrouprole.role_id JOIN authsrv_group ON authsrv_group.id=authsrv_projectgrouprole.group_id JOIN authsrv_project ON authsrv_project.id=authsrv_projectgrouprole.project_id WHERE \(authsrv_projectgrouprole.project_id = '` + uid + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "project", "group"}).AddRow("test-role1", "test-project1", "test-group1")) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, authsrv_project.name as project, authsrv_group.name as group, namespace FROM "authsrv_projectgroupnamespacerole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectgroupnamespacerole.role_id JOIN authsrv_project ON authsrv_project.id=authsrv_projectgroupnamespacerole.project_id JOIN authsrv_group ON authsrv_group.id=authsrv_projectgroupnamespacerole.group_id WHERE \(authsrv_projectgroupnamespacerole.project_id = '` + uid + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "project", "group", "namespace"}).AddRow("test-role2", "test-project2", "test-group2", "test-namespace2")) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, identities.traits ->> 'email' as user FROM "authsrv_projectaccountresourcerole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectaccountresourcerole.role_id JOIN identities ON identities.id=authsrv_projectaccountresourcerole.account_id WHERE \(authsrv_projectaccountresourcerole.project_id = '` + uid + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "user"}).AddRow("test-role3", "test-user3")) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, identities.traits ->> 'email' as user, namespace FROM "authsrv_projectaccountnamespacerole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectaccountnamespacerole.role_id JOIN identities ON identities.id=authsrv_projectaccountnamespacerole.account_id WHERE \(authsrv_projectaccountnamespacerole.project_id = '` + uid + `'\) AND \(authsrv_projectaccountnamespacerole.trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "user", "namespace"}).AddRow("test-role4", "test-user4", "test-namespace4")) + + project := &systemv3.Project{ + Metadata: &v3.Metadata{ + Organization: ouuid, + }, + } + pl, err := ps.List(context.Background(), project) + if err != nil { + t.Fatal("could not get project:", err) + } + if pl.Metadata.Count != 1 { + t.Errorf("incorrect number of projects returned; expected '%v', got '%v'", 1, pl.Metadata.Count) + } + if pl.Items[0].Metadata.Name != "project-"+uid { + t.Errorf("incorrect project name; expected '%v', got '%v'", "project-"+uid, pl.Items[0].Metadata.Name) + } + if *pl.Items[0].Spec.ProjectNamespaceRoles[0].Project != "test-project1" { + t.Errorf("incorrect projectnamespacerole; expected '%v', got '%v'", "test-project1", *pl.Items[0].Spec.ProjectNamespaceRoles[0].Project) + } + if *pl.Items[0].Spec.ProjectNamespaceRoles[1].Namespace != "test-namespace2" { + t.Errorf("incorrect projectnamespacerole; expected '%v', got '%v'", "test-namespace2", *pl.Items[0].Spec.ProjectNamespaceRoles[1].Namespace) + } + if pl.Items[0].Spec.UserRoles[0].User != "test-user3" { + t.Errorf("incorrect userrole; expected '%v', got '%v'", "test-user3", pl.Items[0].Spec.UserRoles[0].User) + } + if pl.Items[0].Spec.UserRoles[1].Namespace != "test-namespace4" { + t.Errorf("incorrect userrole; expected '%v', got '%v'", "test-namespace4", pl.Items[0].Spec.UserRoles[1].Namespace) + } +} + +func TestProjectListNonDev(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + mazc := mockAuthzClient{} + ps := NewProjectService(db, &mazc, getLogger(), false) + + uid := uuid.NewString() + ouuid := addFetchExpectation(mock, "organization") + puuid := addFetchExpectation(mock, "partner") + uuuid := addUserFetchExpectation(mock) + mock.ExpectQuery(`SELECT distinct account_id, project_id FROM "sentry_account_permission" AS "sap" WHERE \(sap.partner_id = '` + puuid + `'\) AND \(sap.organization_id = '` + ouuid + `'\) AND \(sap.account_id = '` + uuuid + `'\) AND \(sap.permission_name IN \('project.read', 'ops_star.all'\)\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"account_id", "project_id"}).AddRow(uuuid, uid)) + mock.ExpectQuery(`SELECT "project"."id", "project"."name", .* FROM "authsrv_project" AS "project" WHERE \(project.partner_id = '` + puuid + `'\) AND \(project.organization_id = '` + ouuid + `'\) AND \(project.trash = FALSE\) AND \(project.id IN \('` + uid + `'\)\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uid, "project-"+uid)) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, authsrv_project.name as project, authsrv_group.name as group FROM "authsrv_projectgrouprole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectgrouprole.role_id JOIN authsrv_group ON authsrv_group.id=authsrv_projectgrouprole.group_id JOIN authsrv_project ON authsrv_project.id=authsrv_projectgrouprole.project_id WHERE \(authsrv_projectgrouprole.project_id = '` + uid + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "project", "group"}).AddRow("test-role1", "test-project1", "test-group1")) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, authsrv_project.name as project, authsrv_group.name as group, namespace FROM "authsrv_projectgroupnamespacerole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectgroupnamespacerole.role_id JOIN authsrv_project ON authsrv_project.id=authsrv_projectgroupnamespacerole.project_id JOIN authsrv_group ON authsrv_group.id=authsrv_projectgroupnamespacerole.group_id WHERE \(authsrv_projectgroupnamespacerole.project_id = '` + uid + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "project", "group", "namespace"}).AddRow("test-role2", "test-project2", "test-group2", "test-namespace2")) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, identities.traits ->> 'email' as user FROM "authsrv_projectaccountresourcerole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectaccountresourcerole.role_id JOIN identities ON identities.id=authsrv_projectaccountresourcerole.account_id WHERE \(authsrv_projectaccountresourcerole.project_id = '` + uid + `'\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "user"}).AddRow("test-role3", "test-user3")) + mock.ExpectQuery(`SELECT distinct authsrv_resourcerole.name as role, identities.traits ->> 'email' as user, namespace FROM "authsrv_projectaccountnamespacerole" JOIN authsrv_resourcerole ON authsrv_resourcerole.id=authsrv_projectaccountnamespacerole.role_id JOIN identities ON identities.id=authsrv_projectaccountnamespacerole.account_id WHERE \(authsrv_projectaccountnamespacerole.project_id = '` + uid + `'\) AND \(authsrv_projectaccountnamespacerole.trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"role", "user", "namespace"}).AddRow("test-role4", "test-user4", "test-namespace4")) + + project := &systemv3.Project{ + Metadata: &v3.Metadata{ + Organization: ouuid, + }, + } + + sd := v3.SessionData{Username: "user-" + uuuid} + ctx := context.WithValue(context.Background(), common.SessionDataKey, &sd) + pl, err := ps.List(ctx, project) + if err != nil { + t.Fatal("could not get project:", err) + } + + if pl.Metadata.Count != 1 { + t.Errorf("incorrect number of projects returned; expected '%v', got '%v'", 1, pl.Metadata.Count) + } + + if pl.Items[0].Metadata.Name != "project-"+uid { + t.Errorf("incorrect project name; expected '%v', got '%v'", "project-"+uid, pl.Items[0].Metadata.Name) + } + + if *pl.Items[0].Spec.ProjectNamespaceRoles[0].Project != "test-project1" { + t.Errorf("incorrect projectnamespacerole; expected '%v', got '%v'", "test-project1", *pl.Items[0].Spec.ProjectNamespaceRoles[0].Project) + } + + if *pl.Items[0].Spec.ProjectNamespaceRoles[1].Namespace != "test-namespace2" { + t.Errorf("incorrect projectnamespacerole; expected '%v', got '%v'", "test-namespace2", *pl.Items[0].Spec.ProjectNamespaceRoles[1].Namespace) + } + + if pl.Items[0].Spec.UserRoles[0].User != "test-user3" { + t.Errorf("incorrect userrole; expected '%v', got '%v'", "test-user3", pl.Items[0].Spec.UserRoles[0].User) + } + + if pl.Items[0].Spec.UserRoles[1].Namespace != "test-namespace4" { + t.Errorf("incorrect userrole; expected '%v', got '%v'", "test-namespace4", pl.Items[0].Spec.UserRoles[1].Namespace) + } +} diff --git a/pkg/service/relay_audit_query.go b/pkg/service/relay_audit_query.go index 4d7df16..f4dbf34 100644 --- a/pkg/service/relay_audit_query.go +++ b/pkg/service/relay_audit_query.go @@ -9,7 +9,7 @@ import ( ) type RelayAuditService struct { - relayQuery *ElasticSearchQuery + relayQuery ElasticSearchQuery } func NewRelayAuditService(url string, auditPattern string, logPrefix string) (*RelayAuditService, error) { diff --git a/pkg/service/relay_audit_query_test.go b/pkg/service/relay_audit_query_test.go new file mode 100644 index 0000000..1a954e3 --- /dev/null +++ b/pkg/service/relay_audit_query_test.go @@ -0,0 +1,149 @@ +package service + +import ( + "encoding/json" + "strings" + "testing" + + v1 "github.com/paralus/paralus/proto/rpc/audit" + v3 "github.com/paralus/paralus/proto/types/commonpb/v3" +) + +type rmd struct { + Source []string `json:"_source"` + Aggs struct { + GroupByCluster struct { + Aggs struct { + GroupByNamespace struct { + Terms struct { + Field string `json:"field"` + Size int `json:"size"` + } `json:"terms"` + } `json:"group_by_namespace"` + GroupByUsername struct { + Terms struct { + Field string `json:"field"` + Size int `json:"size"` + } `json:"terms"` + } `json:"group_by_username"` + } `json:"aggs"` + Terms struct { + Field string `json:"field"` + Size int `json:"size"` + } `json:"terms"` + } `json:"group_by_cluster"` + GroupByKind struct { + Terms struct { + Field string `json:"field"` + } `json:"terms"` + } `json:"group_by_kind"` + GroupByMethod struct { + Terms struct { + Field string `json:"field"` + } `json:"terms"` + } `json:"group_by_method"` + GroupByNamespace struct { + Terms struct { + Field string `json:"field"` + } `json:"terms"` + } `json:"group_by_namespace"` + GroupByUsername struct { + Terms struct { + Field string `json:"field"` + } `json:"terms"` + } `json:"group_by_username"` + } `json:"aggs"` + Query struct { + Bool struct { + Filter struct { + Range struct { + JSONTs struct { + Gte string `json:"gte"` + Lt string `json:"lt"` + } `json:"json.ts"` + } `json:"range"` + } `json:"filter"` + Must []struct { + Term struct { + JSONUn string `json:"json.un"` + } `json:"term,omitempty"` + Terms struct { + JSONProject []string `json:"json.project"` + } `json:"terms,omitempty"` + QueryString struct { + Query string `json:"query"` + } `json:"query_string,omitempty"` + } `json:"must"` + } `json:"bool"` + } `json:"query"` + Size int `json:"size"` + Sort struct { + JSONTs struct { + Order string `json:"order"` + } `json:"json.ts"` + } `json:"sort"` +} + +func TestGetRelayAuditLogByProjectsSimple(t *testing.T) { + esq := &mockElasticSearchQuery{} + al := &RelayAuditService{relayQuery: esq} + req := v1.RelayAuditSearchRequest{ + Filter: &v1.RelayAuditQueryFilter{ + QueryString: "query-string", + Projects: []string{"project-one", "project-two"}, + Timefrom: "now-1h", + Type: "test-type", + User: "test-user", + Client: "test-client", + Cluster: "test-cluster", + Namespace: "test-namespace", + Kind: "test-kind", + Method: "test-method", + DashboardData: true, + }, + } + _, err := al.GetRelayAuditByProjects(&req) + if err != nil { + t.Error("unable to get audit logs") + } + if len(esq.msg) != 1 { + t.Fatalf("incorrect number of searches; expected '%v', got '%v'", 1, len(esq.msg)) + } + m := &rmd{} + err = json.Unmarshal(esq.msg[0].Bytes(), m) + if err != nil { + t.Fatal("unable to unmarshall es request") + } + expected := `{"_source":["json"],"aggs":{"group_by_cluster":{"aggs":{"group_by_namespace":{"terms":{"field":"json.ns","size":1000}},"group_by_username":{"terms":{"field":"json.un","size":1000}}},"terms":{"field":"json.cn","size":1000}},"group_by_kind":{"terms":{"field":"json.k"}},"group_by_method":{"terms":{"field":"json.m"}},"group_by_namespace":{"terms":{"field":"json.ns"}},"group_by_username":{"terms":{"field":"json.un"}}},"query":{"bool":{"filter":{"range":{"json.ts":{"gte":"now-1h","lt":"now"}}},"must":[{"term":{"json.un":"test-user"}},{"term":{"json.cn":"test-cluster"}},{"term":{"json.ns":"test-namespace"}},{"term":{"json.k":"test-kind"}},{"term":{"json.m":"test-method"}},{"terms":{"json.project":["project-one","project-two"]}},{"query_string":{"query":"query-string"}}]}},"size":0,"sort":{"json.ts":{"order":"desc"}}}` + if strings.TrimSpace(esq.msg[0].String()) != expected { + t.Errorf("incorrect es query; expected '%v', got '%v'", expected, strings.TrimSpace(esq.msg[0].String())) + } +} + +func TestGetRelayAuditLogByProjectsNoProject(t *testing.T) { + esq := &mockElasticSearchQuery{} + al := &RelayAuditService{relayQuery: esq} + req := v1.RelayAuditSearchRequest{ + Metadata: &v3.Metadata{UrlScope: "url/project"}, + Filter: &v1.RelayAuditQueryFilter{ + QueryString: "query-string", + }, + } + _, err := al.GetRelayAudit(&req) + if err != nil { + t.Error("unable to get audit logs", err) + } + if len(esq.msg) != 1 { + t.Fatalf("incorrect number of searches; expected '%v', got '%v'", 1, len(esq.msg)) + } + m := &rmd{} + err = json.Unmarshal(esq.msg[0].Bytes(), m) + if err != nil { + t.Fatal("unable to unmarshall es request") + } + + expected := `{"_source":["json"],"aggs":{"group_by_cluster":{"terms":{"field":"json.cn"}},"group_by_kind":{"terms":{"field":"json.k"}},"group_by_method":{"terms":{"field":"json.m"}},"group_by_namespace":{"terms":{"field":"json.ns"}},"group_by_username":{"terms":{"field":"json.un"}}},"query":{"bool":{"must":[{"terms":{"json.project":["project"]}},{"query_string":{"query":"query-string"}}]}},"size":500,"sort":{"json.ts":{"order":"desc"}}}` + if strings.TrimSpace(esq.msg[0].String()) != expected { + t.Errorf("incorrect es query; expected '%v', got '%v'", expected, strings.TrimSpace(esq.msg[0].String())) + } +} diff --git a/pkg/service/rolepermission.go b/pkg/service/rolepermission.go index a8ee8af..d471d05 100644 --- a/pkg/service/rolepermission.go +++ b/pkg/service/rolepermission.go @@ -49,21 +49,6 @@ func (s *rolepermissionService) toV3Rolepermission(rolepermission *rolev3.RolePe return rolepermission } -func (s *rolepermissionService) getPartnerOrganization(ctx context.Context, rolepermission *rolev3.RolePermission) (uuid.UUID, uuid.UUID, error) { - partner := rolepermission.GetMetadata().GetPartner() - org := rolepermission.GetMetadata().GetOrganization() - partnerId, err := dao.GetPartnerId(ctx, s.db, partner) - if err != nil { - return uuid.Nil, uuid.Nil, err - } - organizationId, err := dao.GetOrganizationId(ctx, s.db, org) - if err != nil { - return partnerId, uuid.Nil, err - } - return partnerId, organizationId, nil - -} - func (s *rolepermissionService) GetByName(ctx context.Context, rolepermission *rolev3.RolePermission) (*rolev3.RolePermission, error) { name := rolepermission.GetMetadata().GetName() entity, err := dao.GetByName(ctx, s.db, name, &models.ResourcePermission{}) diff --git a/pkg/service/rolepermission_test.go b/pkg/service/rolepermission_test.go index 2153d33..d99d473 100644 --- a/pkg/service/rolepermission_test.go +++ b/pkg/service/rolepermission_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/paralus/paralus/pkg/query" commonv3 "github.com/paralus/paralus/proto/types/commonpb/v3" + v3 "github.com/paralus/paralus/proto/types/commonpb/v3" rolev3 "github.com/paralus/paralus/proto/types/rolepb/v3" ) @@ -55,3 +56,63 @@ func TestRolePermissionList(t *testing.T) { t.Errorf("incorrect role ids returned when listing") } } + +func TestRolePermissionListWithSelectors(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + rs := NewRolepermissionService(db) + + ruuid1 := uuid.New().String() + ruuid2 := uuid.New().String() + puuid := uuid.New().String() + ouuid := uuid.New().String() + + mock.ExpectQuery(`SELECT authsrv_resourcepermission.name as name, authsrv_resourcepermission.scope as scope FROM "authsrv_resourcepermission" WHERE \(scope = 'NAMESPACE'\) AND \(authsrv_resourcepermission.trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(ruuid1, "role-"+ruuid1)) + + mock.ExpectQuery(`SELECT authsrv_resourcepermission.name as name, authsrv_resourcepermission.scope as scope FROM "authsrv_resourcepermission" WHERE \(scope = 'PROJECT'\) AND \(authsrv_resourcepermission.trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(ruuid2, "role-"+ruuid2)) + + req := &commonv3.QueryOptions{ + Partner: "partner-" + puuid, + Organization: "org-" + ouuid, + Selector: "namespace", + } + rolelist, err := rs.List(context.Background(), query.WithOptions(req)) + if err != nil { + t.Fatal("could not list rolepermissions:", err) + } + if rolelist.Metadata.Count != 2 { + t.Errorf("incorrect number of rolepermissions returned, expected 2; got %v", rolelist.Metadata.Count) + } + if rolelist.Items[0].Metadata.Name != "role-"+ruuid1 || rolelist.Items[1].Metadata.Name != "role-"+ruuid2 { + t.Errorf("incorrect role ids returned when listing") + } +} + +func TestRolePermissionGetByName(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + rs := NewRolepermissionService(db) + + ruuid := uuid.New().String() + + mock.ExpectQuery(`SELECT "resourcepermission"."id", "resourcepermission"."name".* FROM "authsrv_resourcepermission" AS "resourcepermission"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(ruuid, "role-"+ruuid)) + + req := &rolev3.RolePermission{ + Metadata: &v3.Metadata{}, + } + role, err := rs.GetByName(context.Background(), req) + if err != nil { + t.Fatal("could not list rolepermissions:", err) + } + if role.Metadata.Name != "role-"+ruuid { + t.Errorf("incorrect role name; expected '%v', got '%v'", "role-"+ruuid, role.Metadata.Name) + } +} diff --git a/pkg/service/test_utils.go b/pkg/service/test_utils.go index 9e124de..7834977 100644 --- a/pkg/service/test_utils.go +++ b/pkg/service/test_utils.go @@ -12,7 +12,7 @@ import ( func getLogger() *zap.Logger { ao := audit.AuditOptions{ - LogPath: "stdout", + LogPath: "/dev/stdout", MaxSizeMB: 1, MaxBackups: 10, // Should we let sidecar do rotation? MaxAgeDays: 10, // Make these configurable via env @@ -65,6 +65,16 @@ func idnamea(uid string, resource string) *string { return &name } +func addFetchEmptyExpecteation(mock sqlmock.Sqlmock, resource string) { + mock.ExpectQuery(`SELECT "` + resource + `"."id" FROM "authsrv_` + resource + `" AS "` + resource + `"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"})) +} + +func addFailingFetchExpecteation(mock sqlmock.Sqlmock, resource string) { + mock.ExpectQuery(`SELECT "` + resource + `"."id" FROM "authsrv_` + resource + `" AS "` + resource + `"`). + WithArgs().WillReturnError(fmt.Errorf("no data available")) +} + func addFetchIdExpectation(mock sqlmock.Sqlmock, resource string) string { uid := uuid.New().String() mock.ExpectQuery(`SELECT "` + resource + `"."id" FROM "authsrv_` + resource + `" AS "` + resource + `"`). @@ -72,10 +82,22 @@ func addFetchIdExpectation(mock sqlmock.Sqlmock, resource string) string { return uid } +func addFetchByIdExpectation(mock sqlmock.Sqlmock, resource, uid string) { + mock.ExpectQuery(`SELECT "` + resource + `"."id".* FROM "authsrv_` + resource + `" AS "` + resource + `" WHERE .id = '` + uid + `'.`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uid, resource+"-"+uid)) +} + +func addFetchIdByNameExpectation(mock sqlmock.Sqlmock, resource, name string) string { + uid := uuid.NewString() + mock.ExpectQuery(`SELECT "` + resource + `"."id" FROM "authsrv_` + resource + `" AS "` + resource + `" WHERE .name = '` + name + `'.`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uid)) + return uid +} + func addFetchExpectation(mock sqlmock.Sqlmock, resource string) string { uid := uuid.New().String() mock.ExpectQuery(`SELECT "` + resource + `"."id".* FROM "authsrv_` + resource + `" AS "` + resource + `"`). - WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uid, "role-name")) + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(uid, resource+"-name")) return uid } @@ -152,6 +174,13 @@ func addUserFullFetchExpectation(mock sqlmock.Sqlmock) string { return uid } +func addUserFullFetchExpectationWithIdpGroups(mock sqlmock.Sqlmock) string { + uid := uuid.New().String() + mock.ExpectQuery(`SELECT "identities"."id", "identities"."schema_id", "identities"."traits", "identities"."created_at", "identities"."updated_at", "identities"."state", "identities"."state_changed_at", "identities"."nid", "identity_credential"."id" AS "identity_credential__id", "identity_credential"."identity_id" AS "identity_credential__identity_id", "identity_credential"."identity_credential_type_id" AS "identity_credential__identity_credential_type_id", "identity_credential__identity_credential_type"."id" AS "identity_credential__identity_credential_type__id", "identity_credential__identity_credential_type"."name" AS "identity_credential__identity_credential_type__name" FROM "identities" LEFT JOIN "identity_credentials" AS "identity_credential" ON ."identity_credential"."identity_id" = "identities"."id". LEFT JOIN "identity_credential_types" AS "identity_credential__identity_credential_type" ON ."identity_credential__identity_credential_type"."id" = "identity_credential"."identity_credential_type_id". WHERE .traits ->> 'email' = 'user-` + uid + `'.`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "traits", "identity_credential__identity_credential_type__name"}).AddRow(uid, []byte(`{"email":"user-`+uid+`", "first_name": "John", "last_name": "Doe", "description": "The OG user.", "idp_group": "BigShot"}`), "password")) + return uid +} + func addUsersGroupFetchExpectation(mock sqlmock.Sqlmock, user string) string { uid := uuid.New().String() mock.ExpectQuery(`SELECT "group"."id".* FROM "authsrv_group" AS "group" JOIN authsrv_groupaccount ON authsrv_groupaccount.group_id="group".id WHERE .authsrv_groupaccount.account_id = '` + user + `'`). diff --git a/pkg/service/user.go b/pkg/service/user.go index f7e5bef..5233222 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -125,6 +125,9 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u continue } role := pnr.GetRole() + if role == "" { + return &userv3.User{}, nil, fmt.Errorf("cannot use empty role") + } entity, err := dao.GetByName(ctx, db, role, &models.Role{}) if err != nil { return &userv3.User{}, nil, fmt.Errorf("unable to find role '%v'", role) @@ -445,6 +448,7 @@ func (s *userService) Create(ctx context.Context, user *userv3.User) (*userv3.Us if err != nil { tx.Rollback() _log.Warn("unable to commit changes", err) + return &userv3.User{}, err } rl, err := s.ap.GetRecoveryLink(ctx, id) @@ -727,6 +731,7 @@ func (s *userService) Update(ctx context.Context, user *userv3.User) (*userv3.Us if err != nil { tx.Rollback() _log.Warn("unable to commit changes", err) + return &userv3.User{}, fmt.Errorf("unable to update user '%v'", name) } CreateUserAuditEvent(ctx, s.al, s.db, AuditActionUpdate, user.GetMetadata().GetName(), usr.ID, rolesBefore, rolesAfter, groupsBefore, groupsAfter) diff --git a/pkg/service/user_test.go b/pkg/service/user_test.go index 21edbc7..3075dd8 100644 --- a/pkg/service/user_test.go +++ b/pkg/service/user_test.go @@ -3,12 +3,14 @@ package service import ( "context" "fmt" + "strings" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/google/uuid" "github.com/paralus/paralus/pkg/common" "github.com/paralus/paralus/pkg/query" + userrpcv3 "github.com/paralus/paralus/proto/rpc/user" commonv3 "github.com/paralus/paralus/proto/types/commonpb/v3" v3 "github.com/paralus/paralus/proto/types/commonpb/v3" userv3 "github.com/paralus/paralus/proto/types/userpb/v3" @@ -127,7 +129,7 @@ func TestCreateUserWithRole(t *testing.T) { role.Project = &pruuid } if tc.namespace { - var ns string = "ns" + var ns = "ns" role.Namespace = &ns } mock.ExpectQuery(fmt.Sprintf(`INSERT INTO "%v"`, tc.dbname)). @@ -239,6 +241,92 @@ func TestUpdateUserWithGroup(t *testing.T) { performBasicAuthProviderChecks(t, *ap, 0, 1, 0, 0) } +func TestUpdateUserWithIdpGroupPassed(t *testing.T) { + // Having idp groups passed down should not affect, it should come from db + db, mock := getDB(t) + defer db.Close() + + ap := &mockAuthProvider{} + mazc := mockAuthzClient{} + us := NewUserService(ap, db, &mazc, nil, common.CliConfigDownloadData{}, getLogger(), true) + + // performing update + uuuid := addUserFullFetchExpectation(mock) + puuid, ouuid := addParterOrgFetchExpectation(mock) + mock.ExpectBegin() + _ = addUserRoleMappingsUpdateExpectation(mock, uuuid) + addUserGroupMappingsUpdateExpectation(mock, uuuid) + ruuid := addResourceRoleFetchExpectation(mock, "project") + pruuid := addFetchExpectation(mock, "project") + mock.ExpectQuery(`INSERT INTO "authsrv_projectaccountresourcerole"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.New().String())) + // addFetchExpectation(mock, "group") + // mock.ExpectQuery(`INSERT INTO "authsrv_groupaccount"`). + // WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.New().String())) + mock.ExpectCommit() + + var ns = "ns" + user := &userv3.User{ + Metadata: &v3.Metadata{Partner: "partner-" + puuid, Organization: "org-" + ouuid, Name: "user-" + uuuid}, + Spec: &userv3.UserSpec{ + IdpGroups: []string{"group"}, + ProjectNamespaceRoles: []*userv3.ProjectNamespaceRole{{Project: idnamea(pruuid, "project"), Namespace: &ns, Role: idname(ruuid, "role")}}, + }, + } + user, err := us.Update(context.Background(), user) + if err != nil { + t.Fatal("could not create user:", err) + } + performUserBasicChecks(t, user, uuuid) + if user.GetMetadata().GetName() != "user-"+uuuid { + t.Errorf("expected name 'user-%v'; got '%v'", uuuid, user.GetMetadata().GetName()) + } + performBasicAuthProviderChecks(t, *ap, 0, 1, 0, 0) +} + +func TestUpdateUserWithIdpGroupFetched(t *testing.T) { + // Having idp groups passed down should not affect, it should come from db + db, mock := getDB(t) + defer db.Close() + + ap := &mockAuthProvider{} + mazc := mockAuthzClient{} + us := NewUserService(ap, db, &mazc, nil, common.CliConfigDownloadData{}, getLogger(), true) + + // performing update + uuuid := addUserFullFetchExpectationWithIdpGroups(mock) + puuid, ouuid := addParterOrgFetchExpectation(mock) + mock.ExpectBegin() + _ = addUserRoleMappingsUpdateExpectation(mock, uuuid) + addUserGroupMappingsUpdateExpectation(mock, uuuid) + ruuid := addResourceRoleFetchExpectation(mock, "project") + pruuid := addFetchExpectation(mock, "project") + mock.ExpectQuery(`INSERT INTO "authsrv_projectaccountresourcerole"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.New().String())) + addFetchExpectation(mock, "group") + mock.ExpectQuery(`INSERT INTO "authsrv_groupaccount"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uuid.New().String())) + mock.ExpectCommit() + + var ns = "ns" + user := &userv3.User{ + Metadata: &v3.Metadata{Partner: "partner-" + puuid, Organization: "org-" + ouuid, Name: "user-" + uuuid}, + Spec: &userv3.UserSpec{ + IdpGroups: []string{"group"}, + ProjectNamespaceRoles: []*userv3.ProjectNamespaceRole{{Project: idnamea(pruuid, "project"), Namespace: &ns, Role: idname(ruuid, "role")}}, + }, + } + user, err := us.Update(context.Background(), user) + if err != nil { + t.Fatal("could not create user:", err) + } + performUserBasicChecks(t, user, uuuid) + if user.GetMetadata().GetName() != "user-"+uuuid { + t.Errorf("expected name 'user-%v'; got '%v'", uuuid, user.GetMetadata().GetName()) + } + performBasicAuthProviderChecks(t, *ap, 0, 1, 0, 0) +} + func TestUpdateUserInvalid(t *testing.T) { db, mock := getDB(t) defer db.Close() @@ -468,6 +556,9 @@ func TestUserList(t *testing.T) { {"simple list", "", 50, 20, "", "", "", "", []string{}, ""}, {"simple list with type", "", 50, 20, "", "", "", "", []string{}, "password"}, {"sorted list", "", 50, 20, "email", "asc", "", "", []string{}, ""}, + {"sorted list with ALL projects", "", 50, 20, "email", "asc", "", "", []string{"ALL"}, ""}, + {"sorted list with single project", "", 50, 20, "email", "asc", "", "", []string{"project1"}, ""}, + {"sorted list with projects", "", 50, 20, "email", "asc", "", "", []string{"project1", "project2"}, ""}, {"sorted list without dir", "", 50, 20, "email", "", "", "", []string{}, ""}, {"sorted list with q", "filter-query", 50, 20, "email", "asc", "", "", []string{}, ""}, {"sorted list with role", "", 50, 20, "email", "asc", "role-name", "", []string{}, ""}, @@ -505,6 +596,12 @@ func TestUserList(t *testing.T) { if tc.group != "" { addFetchExpectation(mock, "group") } + for _, p := range tc.projects { + if p == "ALL" { + continue + } + addFetchIdByNameExpectation(mock, "project", p) + } if tc.role != "" || tc.group != "" || len(tc.projects) != 0 { addSentryLookupExpectation(mock, []string{uuuid1, uuuid2}, puuid, ouuid) mock.ExpectQuery(`SELECT "identities"."id", .*WHERE .identities.id IN .'` + uuuid1 + `', '` + uuuid2 + `'.. ` + q + order + `LIMIT ` + fmt.Sprint(tc.limit) + ` OFFSET ` + fmt.Sprint(tc.offset)). @@ -544,6 +641,7 @@ func TestUserList(t *testing.T) { Role: tc.role, Group: tc.group, Type: tc.utype, + Project: strings.Join(tc.projects, ","), } userlist, err := us.List(context.Background(), query.WithOptions(qo)) @@ -633,3 +731,110 @@ func TestUserDeleteSelf(t *testing.T) { t.Fatal("user able to delete their own account") } } + +func TestUserForgotPassword(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ap := &mockAuthProvider{} + mazc := mockAuthzClient{} + us := NewUserService(ap, db, &mazc, nil, common.CliConfigDownloadData{}, getLogger(), true) + + uuuid := addUserFetchExpectation(mock) + + fpreq := &userrpcv3.ForgotPasswordRequest{Username: "user-" + uuuid} + fpresp, err := us.ForgotPassword(context.Background(), fpreq) + if err != nil { + t.Fatal("could not fetch password recovery link:", err) + } + if !strings.HasPrefix(fpresp.RecoveryLink, "https://recoverme.testing/") { + t.Error("invalid recovery url generated") + } +} + +func TestUserRetrieveCliConfigGet(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ap := &mockAuthProvider{} + mazc := mockAuthzClient{} + ks := NewApiKeyService(db, getLogger()) + us := NewUserService(ap, db, &mazc, ks, common.CliConfigDownloadData{}, getLogger(), true) + + uuuid := uuid.NewString() + auuid := uuid.NewString() + mock.ExpectQuery(`SELECT sap.* FROM "sentry_account_permission" AS "sap" JOIN authsrv_project as proj ON \(proj.id = sap.project_id\) AND \(proj.default = TRUE\) WHERE \(account_id = '` + uuuid + `'\) LIMIT 1`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"account_id"}).AddRow(uuuid)) + _ = addFetchExpectation(mock, "project") + _ = addFetchExpectation(mock, "organization") + _ = addFetchExpectation(mock, "partner") + + mock.ExpectQuery(`SELECT "apikey"."id", "apikey"."name",.* FROM "authsrv_apikey" AS "apikey" WHERE \(name = 'user-` + uuuid + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"key", "secret"}).AddRow(auuid, "apikey-"+auuid)) + + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + resp, err := us.RetrieveCliConfig(context.Background(), req) + if err != nil { + t.Fatal("could not fetch cli config:", err) + } + if resp.ApiKey != auuid { + t.Error("incorrect apikey generated") + } + if resp.ApiSecret != "apikey-"+auuid { + t.Error("incorrect apisecret generated") + } + if resp.Project != "project-name" { + t.Error("invalid project name") + } + if resp.Organization != "organization-name" { + t.Error("invalid organization name") + } + if resp.Partner != "partner-name" { + t.Error("invalid partner name") + } +} + +func TestUserRetrieveCliConfigCreate(t *testing.T) { + db, mock := getDB(t) + defer db.Close() + + ap := &mockAuthProvider{} + mazc := mockAuthzClient{} + ks := NewApiKeyService(db, getLogger()) + us := NewUserService(ap, db, &mazc, ks, common.CliConfigDownloadData{}, getLogger(), true) + + uuuid := uuid.NewString() + auuid := uuid.NewString() + mock.ExpectQuery(`SELECT sap.* FROM "sentry_account_permission" AS "sap" JOIN authsrv_project as proj ON \(proj.id = sap.project_id\) AND \(proj.default = TRUE\) WHERE \(account_id = '` + uuuid + `'\) LIMIT 1`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"account_id"}).AddRow(uuuid)) + _ = addFetchExpectation(mock, "project") + _ = addFetchExpectation(mock, "organization") + _ = addFetchExpectation(mock, "partner") + + mock.ExpectQuery(`SELECT "apikey"."id", "apikey"."name",.* FROM "authsrv_apikey" AS "apikey" WHERE \(name = 'user-` + uuuid + `'\) AND \(trash = FALSE\)`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + + mock.ExpectQuery(`INSERT INTO "authsrv_apikey"`). + WithArgs().WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(auuid)) + + req := &userrpcv3.ApiKeyRequest{Username: "user-" + uuuid, Id: uuuid} + resp, err := us.RetrieveCliConfig(context.Background(), req) + if err != nil { + t.Fatal("could not fetch cli config:", err) + } + if len(resp.ApiKey) == 0 { + t.Error("no apikey generated") + } + if len(resp.ApiSecret) == 0 { + t.Error("no apisecret generated") + } + if resp.Project != "project-name" { + t.Error("invalid project name") + } + if resp.Organization != "organization-name" { + t.Error("invalid organization name") + } + if resp.Partner != "partner-name" { + t.Error("invalid partner name") + } +}