From 729009d7fb8f16f9702103258d0ac2f52f307786 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 1 Apr 2022 18:44:40 +0530 Subject: [PATCH] Add basic audit logging setup --- internal/dao/common.go | 44 +++++ internal/dao/user.go | 14 ++ master.rest | 27 +++ pkg/audit/events.go | 75 ++++++-- pkg/service/apikey.go | 12 +- pkg/service/audit_utils.go | 369 ++++++++++++++++++++++++++++++++++++ pkg/service/cluster.go | 7 + pkg/service/group.go | 115 +++++++---- pkg/service/idp.go | 7 + pkg/service/organization.go | 15 +- pkg/service/project.go | 11 +- pkg/service/role.go | 25 ++- pkg/service/user.go | 92 +++++---- pkg/service/utils.go | 57 ++++++ server/user.go | 3 +- 15 files changed, 759 insertions(+), 114 deletions(-) create mode 100644 pkg/service/audit_utils.go diff --git a/internal/dao/common.go b/internal/dao/common.go index 91ddf85..58bf7f0 100644 --- a/internal/dao/common.go +++ b/internal/dao/common.go @@ -128,6 +128,22 @@ func GetNameById(ctx context.Context, db bun.IDB, id uuid.UUID, entity interface return entity, nil } +func GetNamesByIds(ctx context.Context, db bun.IDB, id []uuid.UUID, entity interface{}) ([]string, error) { + names := []string{} + if len(id) == 0 { + return names, nil + } + err := db.NewSelect().Column("name").Model(entity). + Where("id = (?)", bun.In(id)). + Where("trash = ?", false). + Scan(ctx, &names) + if err != nil { + return nil, err + } + + return names, nil +} + func Update(ctx context.Context, db bun.IDB, id uuid.UUID, entity interface{}) (interface{}, error) { if _, err := db.NewUpdate().Model(entity).Where("id = ?", id).Exec(ctx); err != nil { return nil, err @@ -147,6 +163,7 @@ func Delete(ctx context.Context, db bun.IDB, id uuid.UUID, entity interface{}) e Model(entity). Column("trash"). Where("id = ?", id). + Where("trash = false"). Set("trash = ?", true). Exec(ctx) return err @@ -157,11 +174,38 @@ func DeleteX(ctx context.Context, db bun.IDB, field string, value interface{}, e Model(entity). Column("trash"). Where("? = ?", bun.Ident(field), value). + Where("trash = false"). Set("trash = ?", true). Exec(ctx) return err } +// DeleteR delete and returns the changed items +func DeleteR(ctx context.Context, db bun.IDB, id uuid.UUID, entity interface{}) error { + _, err := db.NewUpdate(). + Model(entity). + Column("trash"). + Where("id = ?", id). + Where("trash = false"). + Set("trash = ?", true). + Returning("*"). + Exec(ctx) + return err +} + +// DeleteXR delete with selector and returns the changed items +func DeleteXR(ctx context.Context, db bun.IDB, field string, value interface{}, entity interface{}) error { + _, err := db.NewUpdate(). + Model(entity). + Column("trash"). + Where("? = ?", bun.Ident(field), value). + Where("trash = false"). + Set("trash = ?", true). + Returning("*"). + Exec(ctx) + return err +} + // HardDeleteAll deletes all records in a table (primarily for use in scripts) func HardDeleteAll(ctx context.Context, db bun.IDB, entity interface{}) error { _, err := db.NewDelete(). diff --git a/internal/dao/user.go b/internal/dao/user.go index b2861e3..3a0526a 100644 --- a/internal/dao/user.go +++ b/internal/dao/user.go @@ -129,3 +129,17 @@ func ListFilteredUsers(ctx context.Context, db bun.IDB, users *[]models.KratosId } return users, nil } + +func GetUserNamesByIds(ctx context.Context, db bun.IDB, id []uuid.UUID, entity interface{}) ([]string, error) { + names := []string{} + if len(id) == 0 { + return names, nil + } + err := db.NewSelect().ColumnExpr("traits ->> 'email' as name").Model(entity). + Where("id = (?)", bun.In(id)). + Scan(ctx, &names) + if err != nil { + return nil, err + } + return names, nil +} diff --git a/master.rest b/master.rest index 31f36cd..753188c 100644 --- a/master.rest +++ b/master.rest @@ -315,11 +315,20 @@ GET :host/auth/v3/users Content-Type: application/yaml X-Session-Token: :token +# Get all users with query options +GET :host/auth/v3/users?partner=:partner&organization=:org&q=user&name=john&order_by=email&project=ALL,:project +Content-Type: application/yaml +X-Session-Token: :token + # Get single user GET :host/auth/v3/user/:user Content-Type: application/yaml X-Session-Token: :token +# Get currently logged in user info +GET :host/auth/v3/userinfo +Content-Type: application/yaml +X-Session-Token: :token # Delete single user DELETE :host/auth/v3/user/:user @@ -510,6 +519,24 @@ metadata: spec: active: true +# Update organization +PUT :host/auth/v3/partner/:partner/organization/:org +Content-Type: application/yaml +X-Session-Token: :token + +metadata: + partner: :partner + name: :org + description: "Very first organizataion" +spec: + active: true + settings: + idleLogoutMin: 30 + lockout: + enabled: true + period_min: 10 + attempts: 6 + # List organizations GET :host/auth/v3/partner/:partner/organizations Content-Type: application/yaml diff --git a/pkg/audit/events.go b/pkg/audit/events.go index f6ff000..110f4a5 100644 --- a/pkg/audit/events.go +++ b/pkg/audit/events.go @@ -10,7 +10,6 @@ import ( logv2 "github.com/RafayLabs/rcloud-base/pkg/log" commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" - "github.com/Shopify/sarama" "google.golang.org/grpc/metadata" ) @@ -84,7 +83,6 @@ type Event struct { } type createEventOptions struct { - producer sarama.AsyncProducer version EventVersion origin EventOrigin category EventCategory @@ -98,13 +96,6 @@ type createEventOptions struct { groups []string } -// WithProducer sets producer for audit event -func WithProducer(producer sarama.AsyncProducer) CreateEventOption { - return func(opts *createEventOptions) { - opts.producer = producer - } -} - // WithVersion sets version for audit event func WithVersion(version EventVersion) CreateEventOption { return func(opts *createEventOptions) { @@ -193,11 +184,6 @@ func CreateEvent(event *Event, opts ...CreateEventOption) error { opt(&cOpts) } - if cOpts.producer == nil { - _log.Infow("audit event producer is nil") - return fmt.Errorf("audit even producer is nil") - } - t := time.Now() dateArray := strings.Fields(t.String()) timestamp := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.%06d%s", @@ -226,11 +212,7 @@ func CreateEvent(event *Event, opts ...CreateEventOption) error { _log.Infow("unable to marshal audit event", "error", err) return err } - rawMessage := &sarama.ProducerMessage{ - Topic: string(cOpts.topic), - Value: sarama.ByteEncoder(payload), - } - cOpts.producer.Input() <- rawMessage + fmt.Println("event:", string(payload)) // TODO: Switch to writing to audit file return nil } @@ -288,7 +270,7 @@ func getActor(cOpts createEventOptions) *EventActor { } func GetActorFromSessionData(sd *commonv3.SessionData) *EventActor { - pid := sd.GetPartner() + pid := sd.GetPartner() // TODO: have this pulled from headers oid := sd.GetOrganization() accountID := sd.GetAccount() username := sd.GetUsername() @@ -296,12 +278,15 @@ func GetActorFromSessionData(sd *commonv3.SessionData) *EventActor { ID: accountID, Username: username, } - groups := sd.Groups + groups := sd.Groups // TODO: get groups (in interceptor?) // Set org id to string "null" for users with PARTNER_ADMIN role if oid == "" { oid = "null" } + if pid == "" { + pid = "null" + } return &EventActor{ Type: "USER", @@ -321,6 +306,15 @@ func GetClientFromRequest(r *http.Request) *EventClient { } } +func GetClientFromSessionData(sd *commonv3.SessionData) *EventClient { + return &EventClient{ + Type: "BROWSER", + IP: sd.GetClientIp(), + UserAgent: sd.GetClientUa(), + Host: sd.GetClientHost(), + } +} + func GetEvent(r *http.Request, sd *commonv3.SessionData, detail *EventDetail, eventType string, projectID string) *Event { event := &Event{ Actor: GetActorFromSessionData(sd), @@ -333,3 +327,42 @@ func GetEvent(r *http.Request, sd *commonv3.SessionData, detail *EventDetail, ev return event } + +func CreateV1Event(sd *commonv3.SessionData, detail *EventDetail, eventType string, projectID string) error { + actor := GetActorFromSessionData(sd) + client := GetClientFromSessionData(sd) + + if projectID == "" { + projectID = "null" + } + + event := &Event{ + Version: VersionV1, + Category: AuditCategory, + Origin: OriginCore, + Actor: actor, + Client: client, + Detail: detail, + Type: eventType, + Portal: "OPS", // TODO: What is the portal? + ProjectID: projectID, + } + + event.PartnerID = actor.PartnerID + event.OrganizationID = actor.OrganizationID + + t := time.Now() + dateArray := strings.Fields(t.String()) + timestamp := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.%06d%s", + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), dateArray[2]) + event.Timestamp = timestamp + + payload, err := json.Marshal(event) + if err != nil { + _log.Infow("unable to marshal audit event", "error", err) + return err + } + fmt.Println("event:", string(payload)) // TODO: Switch to writing to audit file + return nil +} diff --git a/pkg/service/apikey.go b/pkg/service/apikey.go index dfe325d..027210d 100644 --- a/pkg/service/apikey.go +++ b/pkg/service/apikey.go @@ -49,10 +49,14 @@ func (s *apiKeyService) Create(ctx context.Context, req *rpcv3.ApiKeyRequest) (* Secret: crypto.GenerateSha256Secret(), } - _, err := dao.Create(ctx, s.db, apikey) + entity, err := dao.Create(ctx, s.db, apikey) if err != nil { return nil, err } + + if ak, ok := entity.(*models.Group); ok { + CreateApiKeyAuditEvent(ctx, AuditActionCreate, ak.ID.String()) + } return apikey, nil } @@ -61,6 +65,12 @@ func (s *apiKeyService) Delete(ctx context.Context, req *rpcv3.ApiKeyRequest) (* Set("trash = ?", true). Where("account_id = ?", req.Username). Where("key = ?", req.Id).Exec(ctx) + if err != nil { + return &rpcv3.DeleteUserResponse{}, err + } + + + CreateApiKeyAuditEvent(ctx, AuditActionDelete, req.Id) return nil, err } diff --git a/pkg/service/audit_utils.go b/pkg/service/audit_utils.go new file mode 100644 index 0000000..97bbc02 --- /dev/null +++ b/pkg/service/audit_utils.go @@ -0,0 +1,369 @@ +package service + +import ( + "context" + "fmt" + "strings" + + "github.com/RafayLabs/rcloud-base/internal/dao" + "github.com/RafayLabs/rcloud-base/internal/models" + "github.com/RafayLabs/rcloud-base/pkg/audit" + systemv3 "github.com/RafayLabs/rcloud-base/proto/types/systempb/v3" + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +const ( + AuditActionCreate = "create" + AuditActionDelete = "delete" + AuditActionUpdate = "update" +) + +// TODO: add audit events for user-role or user-group mappings +func CreateUserAuditEvent(ctx context.Context, db bun.IDB, action string, name string, id uuid.UUID, rolesBefore, rolesAfter []uuid.UUID) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("User %s %sd", name, action), + Meta: map[string]string{ + "account_id": id.String(), + "username": name, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("user.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } + + cr, _, dr := diffu(rolesBefore, rolesAfter) + ncr, err := dao.GetNamesByIds(ctx, db, cr, &models.Role{}) + if err != nil { + _log.Warn("unable to create audit event", err) + } + ndr, err := dao.GetNamesByIds(ctx, db, dr, &models.Role{}) + if err != nil { + _log.Warn("unable to create audit event", err) + } + for _, r := range ncr { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Role %s added to user %s", r, name), + Meta: map[string]string{ + "username": name, + "roles_name": r, // TODO: add info like namespace and project + }, + } + // user.role.created is user.project.created in rcloud + if err := audit.CreateV1Event(sd, detail, "user.role.created", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } + + for _, r := range ndr { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Role %s deleted from user %s", r, name), + Meta: map[string]string{ + "username": name, + "role_name": r, + }, + } + if err := audit.CreateV1Event(sd, detail, "user.role.deleted", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } +} + +func CreateGroupAuditEvent(ctx context.Context, db bun.IDB, action string, name string, id uuid.UUID, usersBefore, usersAfter, rolesBefore, rolesAfter []uuid.UUID) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Group %s %sd", name, action), + Meta: map[string]string{ + "group_id": id.String(), + "group_name": name, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("group.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } + + cu, _, du := diffu(usersBefore, usersAfter) + + cun, err := dao.GetUserNamesByIds(ctx, db, cu, &models.KratosIdentities{}) + if err != nil { + _log.Warn("unable to create audit event", err) + } + dun, err := dao.GetUserNamesByIds(ctx, db, du, &models.KratosIdentities{}) + if err != nil { + _log.Warn("unable to create audit event", err) + } + + for _, u := range cun { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("User %s added to group %s", u, name), + Meta: map[string]string{ + "group_id": id.String(), + "group_name": name, + "username": u, + }, + } + if err := audit.CreateV1Event(sd, detail, "group.user.created", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } + + for _, u := range dun { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("User %s deleted from group %s", u, name), + Meta: map[string]string{ + "group_id": id.String(), + "group_name": name, + "username": u, + }, + } + if err := audit.CreateV1Event(sd, detail, "group.user.deleted", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } + + cr, _, dr := diffu(rolesBefore, rolesAfter) + ncr, err := dao.GetNamesByIds(ctx, db, cr, &models.Role{}) + if err != nil { + _log.Warn("unable to create audit event", err) + } + ndr, err := dao.GetNamesByIds(ctx, db, dr, &models.Role{}) + if err != nil { + _log.Warn("unable to create audit event", err) + } + for _, r := range ncr { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Role %s added to group %s", r, name), + Meta: map[string]string{ + "group_id": id.String(), + "group_name": name, + "roles_name": r, // TODO: add info like namespace and project + }, + } + // group.role.created is group.project.created in rcloud + if err := audit.CreateV1Event(sd, detail, "group.role.created", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } + + for _, r := range ndr { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Role %s deleted from group %s", r, name), + Meta: map[string]string{ + "group_id": id.String(), + "group_name": name, + "role_name": r, + }, + } + if err := audit.CreateV1Event(sd, detail, "group.role.deleted", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } + +} + +func CreateRoleAuditEvent(ctx context.Context, action string, name string, id uuid.UUID, permissions []string) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Role %s %sd", name, action), + Meta: map[string]string{ + "role_id": id.String(), + "role_name": name, + "permissions": strings.Join(permissions, ","), // TODO: Should we split it into individual ones? + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("role.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } +} + +func CreateProjectAuditEvent(ctx context.Context, action string, name string, id uuid.UUID) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Project %s %sd", name, action), + Meta: map[string]string{ + "project_id": id.String(), + "project_name": name, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("project.%s.success", action), id.String()); err != nil { + _log.Warn("unable to create audit event", err) + } +} + +func CreateOrganizationAuditEvent(ctx context.Context, action string, name string, id uuid.UUID, settingsBefore, settingsAfter *systemv3.OrganizationSettings) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Organization %s %sd", name, action), + Meta: map[string]string{ + "organization_id": id.String(), + "organization_name": name, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("organization.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } + + if settingsBefore == nil && settingsAfter == nil { + return + } + + bavail := settingsBefore != nil && settingsAfter != nil + if !bavail || settingsBefore.IdleLogoutMin != settingsAfter.IdleLogoutMin { + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Idel logout settings updated for organization %s", name), + Meta: map[string]string{ + "organization_id": id.String(), + "organization_name": name, + }, + } + + if settingsAfter != nil { + detail.Meta = map[string]string{ + "organization_id": id.String(), + "organization_name": name, + "idle_logout_min": string(settingsAfter.IdleLogoutMin), + } + } + + if err := audit.CreateV1Event(sd, detail, "organization.idle.timeout.settings.updated", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } + + bavail = bavail && settingsBefore.Lockout != nil && settingsAfter.Lockout != nil + + if !bavail || + settingsBefore.Lockout.Enabled != settingsAfter.Lockout.Enabled || + settingsBefore.Lockout.PeriodMin != settingsAfter.Lockout.PeriodMin || + settingsBefore.Lockout.Attempts != settingsAfter.Lockout.Attempts { + + enabled := "false" + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Lockout settings updated for organization %s", name), + Meta: map[string]string{ + "organization_id": id.String(), + "organization_name": name, + }, + } + + if settingsAfter != nil && settingsAfter.Lockout != nil { + if settingsAfter.Lockout.Enabled { + enabled = "true" + } + detail.Meta = map[string]string{ + "organization_id": id.String(), + "organization_name": name, + "lockout_enabled": enabled, + "lockout_period_min": string(settingsAfter.Lockout.PeriodMin), + "lockout_attempts": string(settingsAfter.Lockout.Attempts), + } + } + + if err := audit.CreateV1Event(sd, detail, "organization.lockout.settings.updated", ""); err != nil { + _log.Warn("unable to create audit event", err) + } + } +} + +func CreateIdpAuditEvent(ctx context.Context, action string, name string, id uuid.UUID) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Idp %s %sd", name, action), + Meta: map[string]string{ + "idp_id": id.String(), + "idp_name": name, + }, + } + // TODO: it is idp.config.created in rcloud + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("idp.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } +} + +func CreateApiKeyAuditEvent(ctx context.Context, action string, id string) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("ApiKey %s %sd", id, action), + Meta: map[string]string{ + "apikey": id, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("apikey.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } +} + +func CreateClusterAuditEvent(ctx context.Context, action string, name string, id uuid.UUID) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Cluster %s %sd", name, action), + Meta: map[string]string{ + "cluster_id": id.String(), + "cluster_name": name, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("cluster.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } +} + +// TODO: figure out how this is to be added +func CreateLocationAuditEvent(ctx context.Context, action string, name string, id uuid.UUID) { + sd, ok := GetSessionDataFromContext(ctx) + if !ok { + _log.Warn("unable to create audit event: could not fetch info from context") + return + } + + detail := &audit.EventDetail{ + Message: fmt.Sprintf("Location %s %sd", name, action), + Meta: map[string]string{ + "location_id": id.String(), + "location_name": name, + }, + } + if err := audit.CreateV1Event(sd, detail, fmt.Sprintf("location.%s.success", action), ""); err != nil { + _log.Warn("unable to create audit event", err) + } +} diff --git a/pkg/service/cluster.go b/pkg/service/cluster.go index 983383b..19ed017 100644 --- a/pkg/service/cluster.go +++ b/pkg/service/cluster.go @@ -309,6 +309,7 @@ func (es *clusterService) Create(ctx context.Context, cluster *infrav3.Cluster) h.OnChange(ev) } + CreateClusterAuditEvent(ctx, AuditActionCreate, clusterResp.GetMetadata().GetName(), edb.ID) return clusterResp, nil } @@ -590,6 +591,8 @@ func (cs *clusterService) Update(ctx context.Context, cluster *infrav3.Cluster) h.OnChange(ev) }*/ + CreateClusterAuditEvent(ctx, AuditActionUpdate, cluster.GetMetadata().GetName(), cdb.ID) + return cluster, nil } @@ -634,6 +637,10 @@ func (cs *clusterService) Delete(ctx context.Context, cluster *infrav3.Cluster) h.OnChange(ev) } + id, err := uuid.Parse(clusterId) + if err == nil { + CreateClusterAuditEvent(ctx, AuditActionDelete, cluster.GetMetadata().GetName(), id) + } return nil } diff --git a/pkg/service/group.go b/pkg/service/group.go index 44cfb7b..f3df26c 100644 --- a/pkg/service/group.go +++ b/pkg/service/group.go @@ -51,42 +51,59 @@ func NewGroupService(db *bun.DB, azc AuthzService) GroupService { return &groupService{db: db, azc: azc} } -func (s *groupService) deleteGroupRoleRelaitons(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) { - // delete previous entries +// deleteGroupRoleRelaitons deletes existing group-role relations +func (s *groupService) deleteGroupRoleRelaitons(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, []uuid.UUID, error) { // TODO: single delete command - err := dao.DeleteX(ctx, db, "group_id", groupId, &models.GroupRole{}) + ids := []uuid.UUID{} + gr := []models.GroupRole{} + err := dao.DeleteXR(ctx, db, "group_id", groupId, &gr) if err != nil { - return &userv3.Group{}, err + return &userv3.Group{}, nil, err } - err = dao.DeleteX(ctx, db, "group_id", groupId, &models.ProjectGroupRole{}) - if err != nil { - return &userv3.Group{}, err + for _, r := range gr { + ids = append(ids, r.RoleId) } - err = dao.DeleteX(ctx, db, "group_id", groupId, &models.ProjectGroupNamespaceRole{}) + + pgr := []models.ProjectGroupRole{} + err = dao.DeleteX(ctx, db, "group_id", groupId, &pgr) if err != nil { - return &userv3.Group{}, err + return &userv3.Group{}, nil, err + } + for _, r := range pgr { + ids = append(ids, r.RoleId) + } + + pgnr := []models.ProjectGroupNamespaceRole{} + err = dao.DeleteX(ctx, db, "group_id", groupId, &pgnr) + if err != nil { + return &userv3.Group{}, nil, err + } + for _, r := range pgnr { + ids = append(ids, r.RoleId) } _, err = s.azc.DeletePolicies(ctx, &authzv1.Policy{Sub: "g:" + group.GetMetadata().GetName()}) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to delete group-role relations from authz; %v", err) + return &userv3.Group{}, nil, fmt.Errorf("unable to delete group-role relations from authz; %v", err) } - return group, nil + + return group, ids, nil } // Map roles to groups -func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, group *userv3.Group, ids parsedIds) (*userv3.Group, error) { +func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, group *userv3.Group, ids parsedIds) (*userv3.Group, []uuid.UUID, error) { // TODO: add transactions projectNamespaceRoles := group.GetSpec().GetProjectNamespaceRoles() var pgrs []models.ProjectGroupRole var grs []models.GroupRole var ps []*authzv1.Policy + var rids []uuid.UUID for _, pnr := range projectNamespaceRoles { role := pnr.GetRole() entity, err := dao.GetByName(ctx, db, role, &models.Role{}) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role) + return &userv3.Group{}, nil, fmt.Errorf("unable to find role '%v'", role) } var roleId uuid.UUID var roleName string @@ -94,9 +111,10 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, if rle, ok := entity.(*models.Role); ok { roleId = rle.ID roleName = rle.Name + rids = append(rids, rle.ID) scope = strings.ToLower(rle.Scope) } else { - return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role) + return &userv3.Group{}, nil, fmt.Errorf("unable to find role '%v'", role) } project := pnr.GetProject() @@ -122,7 +140,7 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, }) case "organization": if org == "" { - return &userv3.Group{}, fmt.Errorf("no org name provided for role '%v'", roleName) + return &userv3.Group{}, nil, fmt.Errorf("no org name provided for role '%v'", roleName) } gr := models.GroupRole{ Trash: false, @@ -142,14 +160,14 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, }) case "project": if org == "" { - return &userv3.Group{}, fmt.Errorf("no org name provided for role '%v'", roleName) + return &userv3.Group{}, nil, fmt.Errorf("no org name provided for role '%v'", roleName) } if project == "" { - return &userv3.Group{}, fmt.Errorf("no project name provided for role '%v'", roleName) + return &userv3.Group{}, nil, fmt.Errorf("no project name provided for role '%v'", roleName) } projectId, err := dao.GetProjectId(ctx, s.db, project) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project) + return &userv3.Group{}, nil, fmt.Errorf("unable to find project '%v'", project) } pgr := models.ProjectGroupRole{ Trash: false, @@ -174,49 +192,56 @@ func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, if len(pgrs) > 0 { _, err := dao.Create(ctx, db, &pgrs) if err != nil { - return &userv3.Group{}, err + return &userv3.Group{}, nil, err } } if len(grs) > 0 { _, err := dao.Create(ctx, db, &grs) if err != nil { - return &userv3.Group{}, err + return &userv3.Group{}, nil, err } } if len(ps) > 0 { success, err := s.azc.CreatePolicies(ctx, &authzv1.Policies{Policies: ps}) if err != nil || !success.Res { - return &userv3.Group{}, fmt.Errorf("unable to create mapping in authz; %v", err) + return &userv3.Group{}, nil, fmt.Errorf("unable to create mapping in authz; %v", err) } } - return group, nil + return group, rids, nil } -func (s *groupService) deleteGroupAccountRelations(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) { - err := dao.DeleteX(ctx, db, "group_id", groupId, &models.GroupAccount{}) +func (s *groupService) deleteGroupAccountRelations(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, []uuid.UUID, error) { + ga := []models.GroupAccount{} + err := dao.DeleteXR(ctx, db, "group_id", groupId, &ga) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to delete user; %v", err) + return &userv3.Group{}, nil, fmt.Errorf("unable to remove user from group user; %v", err) } _, err = s.azc.DeleteUserGroups(ctx, &authzv1.UserGroup{Grp: "g:" + group.GetMetadata().GetName()}) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to delete group-user relations from authz; %v", err) + return &userv3.Group{}, nil, fmt.Errorf("unable to delete group-user relations from authz; %v", err) } - return group, nil + + ids := []uuid.UUID{} + for _, r := range ga { + ids = append(ids, r.AccountId) + } + return group, ids, nil } // Update the users(account) mapped to each group -func (s *groupService) createGroupAccountRelations(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) { +func (s *groupService) createGroupAccountRelations(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, []uuid.UUID, error) { // TODO: add transactions var grpaccs []models.GroupAccount var ugs []*authzv1.UserGroup + var uids []uuid.UUID for _, account := range unique(group.GetSpec().GetUsers()) { // FIXME: do combined lookup entity, err := dao.GetIdByTraits(ctx, db, account, &models.KratosIdentities{}) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to find user '%v'", account) + return &userv3.Group{}, nil, fmt.Errorf("unable to find user '%v'", account) } if acc, ok := entity.(*models.KratosIdentities); ok { grp := models.GroupAccount{ @@ -227,6 +252,7 @@ func (s *groupService) createGroupAccountRelations(ctx context.Context, db bun.I GroupId: groupId, Active: true, } + uids = append(uids, acc.ID) grpaccs = append(grpaccs, grp) ugs = append(ugs, &authzv1.UserGroup{ Grp: "g:" + group.GetMetadata().GetName(), @@ -235,21 +261,21 @@ func (s *groupService) createGroupAccountRelations(ctx context.Context, db bun.I } } if len(grpaccs) == 0 { - return group, nil + return group, nil, nil } _, err := dao.Create(ctx, db, &grpaccs) if err != nil { - return &userv3.Group{}, err + return &userv3.Group{}, nil, err } // TODO: revert our db inserts if this fails // Just FYI, the success can be false if we delete the db directly but casbin has it available internally _, err = s.azc.CreateUserGroups(ctx, &authzv1.UserGroups{UserGroups: ugs}) if err != nil { - return &userv3.Group{}, fmt.Errorf("unable to create mapping in authz; %v", err) + return &userv3.Group{}, nil, fmt.Errorf("unable to create mapping in authz; %v", err) } - return group, nil + return group, uids, nil } // TODO: move this to utils, make it accept two strings (names) @@ -303,13 +329,13 @@ func (s *groupService) Create(ctx context.Context, group *userv3.Group) (*userv3 //update v3 spec if grp, ok := entity.(*models.Group); ok { // we can get previous group using the id, find users/roles from that and delete those - group, err = s.createGroupAccountRelations(ctx, tx, grp.ID, group) + group, usersAfter, err := s.createGroupAccountRelations(ctx, tx, grp.ID, group) if err != nil { tx.Rollback() return &userv3.Group{}, err } - group, err = s.createGroupRoleRelations(ctx, tx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId}) + group, rolesAfter, err := s.createGroupRoleRelations(ctx, tx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId}) if err != nil { tx.Rollback() return &userv3.Group{}, err @@ -320,9 +346,10 @@ func (s *groupService) Create(ctx context.Context, group *userv3.Group) (*userv3 tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateGroupAuditEvent(ctx, s.db, AuditActionCreate, group.GetMetadata().GetName(), grp.ID, []uuid.UUID{}, usersAfter, []uuid.UUID{}, rolesAfter) return group, nil } - tx.Rollback() return &userv3.Group{}, fmt.Errorf("unable to create group") } @@ -423,22 +450,22 @@ func (s *groupService) Update(ctx context.Context, group *userv3.Group) (*userv3 } // update account/role links - group, err = s.deleteGroupAccountRelations(ctx, tx, grp.ID, group) + group, usersBefore, err := s.deleteGroupAccountRelations(ctx, tx, grp.ID, group) if err != nil { tx.Rollback() return &userv3.Group{}, err } - group, err = s.createGroupAccountRelations(ctx, tx, grp.ID, group) + group, usersAfter, err := s.createGroupAccountRelations(ctx, tx, grp.ID, group) if err != nil { tx.Rollback() return &userv3.Group{}, err } - group, err = s.deleteGroupRoleRelaitons(ctx, tx, grp.ID, group) + group, rolesBefore, err := s.deleteGroupRoleRelaitons(ctx, tx, grp.ID, group) if err != nil { tx.Rollback() return &userv3.Group{}, err } - group, err = s.createGroupRoleRelations(ctx, tx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId}) + group, rolesAfter, err := s.createGroupRoleRelations(ctx, tx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId}) if err != nil { tx.Rollback() return &userv3.Group{}, err @@ -462,6 +489,8 @@ func (s *groupService) Update(ctx context.Context, group *userv3.Group) (*userv3 Users: group.Spec.Users, // TODO: update from db resp or no update? ProjectNamespaceRoles: group.Spec.ProjectNamespaceRoles, } + + CreateGroupAuditEvent(ctx, s.db, AuditActionUpdate, group.GetMetadata().GetName(), grp.ID, usersBefore, usersAfter, rolesBefore, rolesAfter) } return group, nil @@ -484,12 +513,12 @@ func (s *groupService) Delete(ctx context.Context, group *userv3.Group) (*userv3 return &userv3.Group{}, err } - group, err = s.deleteGroupRoleRelaitons(ctx, tx, grp.ID, group) + group, rolesBefore, err := s.deleteGroupRoleRelaitons(ctx, tx, grp.ID, group) if err != nil { tx.Rollback() return &userv3.Group{}, err } - group, err = s.deleteGroupAccountRelations(ctx, tx, grp.ID, group) + group, usersBefore, err := s.deleteGroupAccountRelations(ctx, tx, grp.ID, group) if err != nil { tx.Rollback() return &userv3.Group{}, err @@ -505,6 +534,8 @@ func (s *groupService) Delete(ctx context.Context, group *userv3.Group) (*userv3 tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateGroupAuditEvent(ctx, s.db, AuditActionDelete, group.GetMetadata().GetName(), grp.ID, usersBefore, []uuid.UUID{}, rolesBefore, []uuid.UUID{}) return group, nil } diff --git a/pkg/service/idp.go b/pkg/service/idp.go index 6773263..bddf643 100644 --- a/pkg/service/idp.go +++ b/pkg/service/idp.go @@ -205,6 +205,9 @@ func (s *idpService) Create(ctx context.Context, idp *systemv3.Idp) (*systemv3.I SpEntityId: acsURL, }, } + + CreateIdpAuditEvent(ctx, AuditActionCreate, rv.GetMetadata().GetName(), entity.Id) + return rv, nil } @@ -381,6 +384,8 @@ func (s *idpService) Update(ctx context.Context, idp *systemv3.Idp) (*systemv3.I SpEntityId: acsURL, }, } + + CreateIdpAuditEvent(ctx, AuditActionUpdate, rv.GetMetadata().GetName(), entity.Id) return rv, nil } @@ -452,5 +457,7 @@ func (s *idpService) Delete(ctx context.Context, idp *systemv3.Idp) error { if err != nil { return err } + + CreateIdpAuditEvent(ctx, AuditActionDelete, name, entity.Id) return nil } diff --git a/pkg/service/organization.go b/pkg/service/organization.go index d8c832f..c9b2811 100644 --- a/pkg/service/organization.go +++ b/pkg/service/organization.go @@ -99,6 +99,8 @@ func (s *organizationService) Create(ctx context.Context, org *systemv3.Organiza if createdOrg, ok := entity.(*models.Organization); ok { //update v3 spec org.Metadata.Id = createdOrg.ID.String() + + CreateOrganizationAuditEvent(ctx, AuditActionCreate, org.GetMetadata().GetName(), createdOrg.ID, nil, org.GetSpec().GetSettings()) } return org, nil @@ -195,8 +197,11 @@ func (s *organizationService) Update(ctx context.Context, organization *systemv3 } if org, ok := entity.(*models.Organization); ok { + settingsAfter := organization.GetSpec().GetSettings() + settingsBefore := systemv3.OrganizationSettings{} + _ = json.Unmarshal(org.Settings, &settingsBefore) // ignore any unmarshelling issues - sb, err := json.MarshalIndent(organization.GetSpec().GetSettings(), "", "\t") + sb, err := json.MarshalIndent(settingsAfter, "", "\t") if err != nil { return &systemv3.Organization{}, err } @@ -226,6 +231,8 @@ func (s *organizationService) Update(ctx context.Context, organization *systemv3 if err != nil { return &systemv3.Organization{}, err } + + CreateOrganizationAuditEvent(ctx, AuditActionUpdate, organization.GetMetadata().GetName(), org.ID, &settingsBefore, settingsAfter) } return organization, nil @@ -239,7 +246,7 @@ func (s *organizationService) Delete(ctx context.Context, organization *systemv3 } if org, ok := entity.(*models.Organization); ok { - err := dao.Delete(ctx, s.db, org.ID, org) + err := dao.DeleteR(ctx, s.db, org.ID, org) if err != nil { return &systemv3.Organization{}, err } @@ -247,6 +254,10 @@ func (s *organizationService) Delete(ctx context.Context, organization *systemv3 //update v3 status organization.Metadata.Name = org.Name organization.Metadata.ModifiedAt = timestamppb.New(org.ModifiedAt) + + orgSettings := systemv3.OrganizationSettings{} + _ = json.Unmarshal(org.Settings, &orgSettings) // ignore any unmarshelling issues + CreateOrganizationAuditEvent(ctx, AuditActionDelete, organization.GetMetadata().GetName(), org.ID, &orgSettings, nil) } return organization, nil diff --git a/pkg/service/project.go b/pkg/service/project.go index 0cee32a..6507cae 100644 --- a/pkg/service/project.go +++ b/pkg/service/project.go @@ -8,9 +8,7 @@ import ( "github.com/RafayLabs/rcloud-base/internal/dao" "github.com/RafayLabs/rcloud-base/internal/models" - "github.com/RafayLabs/rcloud-base/pkg/common" authzv1 "github.com/RafayLabs/rcloud-base/proto/types/authz" - commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" v3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" systemv3 "github.com/RafayLabs/rcloud-base/proto/types/systempb/v3" "github.com/google/uuid" @@ -105,6 +103,8 @@ func (s *projectService) Create(ctx context.Context, project *systemv3.Project) project.Spec = &systemv3.ProjectSpec{ Default: createdProject.Default, } + + CreateProjectAuditEvent(ctx, AuditActionCreate, project.GetMetadata().GetName(), createdProject.ID) } err = tx.Commit() if err != nil { @@ -282,6 +282,8 @@ func (s *projectService) Update(ctx context.Context, project *systemv3.Project) tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateProjectAuditEvent(ctx, AuditActionUpdate, project.GetMetadata().GetName(), proj.ID) } return project, nil @@ -324,14 +326,17 @@ func (s *projectService) Delete(ctx context.Context, project *systemv3.Project) if err != nil { tx.Rollback() _log.Warn("unable to commit changes", err) + return &systemv3.Project{}, err } + + CreateProjectAuditEvent(ctx, AuditActionDelete, project.GetMetadata().GetName(), proj.ID) } return project, nil } func (s *projectService) List(ctx context.Context, project *systemv3.Project) (*systemv3.ProjectList, error) { - sd, ok := ctx.Value(common.SessionDataKey).(*commonv3.SessionData) + sd, ok := GetSessionDataFromContext(ctx) username := "" if !ok { return &systemv3.ProjectList{}, fmt.Errorf("cannot perform project listing without auth") diff --git a/pkg/service/role.go b/pkg/service/role.go index 6e5d5cd..82891d0 100644 --- a/pkg/service/role.go +++ b/pkg/service/role.go @@ -166,17 +166,20 @@ func (s *roleService) Create(ctx context.Context, role *rolev3.Role) (*rolev3.Ro tx.Rollback() return &rolev3.Role{}, err } - } else { - tx.Rollback() - return &rolev3.Role{}, fmt.Errorf("unable to create role '%v'", role.GetMetadata().GetName()) + + err = tx.Commit() + if err != nil { + tx.Rollback() + _log.Warn("unable to commit changes", err) + } + + CreateRoleAuditEvent(ctx, AuditActionCreate, role.GetMetadata().GetName(), createdRole.ID, role.GetSpec().GetRolepermissions()) + + return role, nil } - err = tx.Commit() - if err != nil { - tx.Rollback() - _log.Warn("unable to commit changes", err) - } - return role, nil + tx.Rollback() + return &rolev3.Role{}, fmt.Errorf("unable to create role '%v'", role.GetMetadata().GetName()) } @@ -279,6 +282,8 @@ func (s *roleService) Update(ctx context.Context, role *rolev3.Role) (*rolev3.Ro tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateRoleAuditEvent(ctx, AuditActionUpdate, role.GetMetadata().GetName(), rle.ID, role.GetSpec().GetRolepermissions()) return role, nil } return &rolev3.Role{}, fmt.Errorf("unable to update role '%v'", role.GetMetadata().GetName()) @@ -321,6 +326,8 @@ func (s *roleService) Delete(ctx context.Context, role *rolev3.Role) (*rolev3.Ro tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateRoleAuditEvent(ctx, AuditActionDelete, role.GetMetadata().GetName(), rle.ID, []string{}) return role, nil } diff --git a/pkg/service/user.go b/pkg/service/user.go index 46b4186..caeea9d 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -102,18 +102,19 @@ func getUserTraits(traits map[string]interface{}) userTraits { } // Map roles to accounts -func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, user *userv3.User, ids parsedIds) (*userv3.User, error) { +func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, user *userv3.User, ids parsedIds) (*userv3.User, []uuid.UUID, error) { projectNamespaceRoles := user.GetSpec().GetProjectNamespaceRoles() // TODO: add transactions var pars []models.ProjectAccountResourcerole var ars []models.AccountResourcerole var ps []*authzv1.Policy + var rids []uuid.UUID for _, pnr := range projectNamespaceRoles { role := pnr.GetRole() entity, err := dao.GetByName(ctx, db, role, &models.Role{}) if err != nil { - return &userv3.User{}, fmt.Errorf("unable to find role '%v'", role) + return &userv3.User{}, nil, fmt.Errorf("unable to find role '%v'", role) } var roleId uuid.UUID var roleName string @@ -121,9 +122,10 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u if rle, ok := entity.(*models.Role); ok { roleId = rle.ID roleName = rle.Name + rids = append(rids, rle.ID) scope = strings.ToLower(rle.Scope) } else { - return &userv3.User{}, fmt.Errorf("unable to find role '%v'", role) + return &userv3.User{}, nil, fmt.Errorf("unable to find role '%v'", role) } project := pnr.GetProject() @@ -153,7 +155,7 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u }) case "organization": if org == "" { - return &userv3.User{}, fmt.Errorf("no org name provided for role '%v'", roleName) + return &userv3.User{}, nil, fmt.Errorf("no org name provided for role '%v'", roleName) } ar := models.AccountResourcerole{ @@ -178,14 +180,14 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u }) case "project": if org == "" { - return &userv3.User{}, fmt.Errorf("no org name provided for role '%v'", roleName) + return &userv3.User{}, nil, fmt.Errorf("no org name provided for role '%v'", roleName) } if project == "" { - return &userv3.User{}, fmt.Errorf("no project name provided for role '%v'", roleName) + return &userv3.User{}, nil, fmt.Errorf("no project name provided for role '%v'", roleName) } projectId, err := dao.GetProjectId(ctx, db, project) if err != nil { - return user, fmt.Errorf("unable to find project '%v'", project) + return user, nil, fmt.Errorf("unable to find project '%v'", project) } par := models.ProjectAccountResourcerole{ CreatedAt: time.Now(), @@ -210,31 +212,31 @@ func (s *userService) createUserRoleRelations(ctx context.Context, db bun.IDB, u }) default: if err != nil { - return user, fmt.Errorf("namespace specific roles are not handled") + return user, nil, fmt.Errorf("namespace specific roles are not handled") } } } if len(pars) > 0 { _, err := dao.Create(ctx, db, &pars) if err != nil { - return &userv3.User{}, err + return &userv3.User{}, nil, err } } if len(ars) > 0 { _, err := dao.Create(ctx, db, &ars) if err != nil { - return &userv3.User{}, err + return &userv3.User{}, nil, err } } if len(ps) > 0 { success, err := s.azc.CreatePolicies(ctx, &authzv1.Policies{Policies: ps}) if err != nil || !success.Res { - return &userv3.User{}, fmt.Errorf("unable to create mapping in authz; %v", err) + return &userv3.User{}, nil, fmt.Errorf("unable to create mapping in authz; %v", err) } } - return user, nil + return user, rids, nil } // Update the groups mapped to each user(account) @@ -336,7 +338,7 @@ func (s *userService) Create(ctx context.Context, user *userv3.User) (*userv3.Us return &userv3.User{}, err } - user, err = s.createUserRoleRelations(ctx, tx, user, parsedIds{Id: uid, Partner: partnerId, Organization: organizationId}) + user, rolesAfter, err := s.createUserRoleRelations(ctx, tx, user, parsedIds{Id: uid, Partner: partnerId, Organization: organizationId}) if err != nil { tx.Rollback() return &userv3.User{}, err @@ -361,6 +363,7 @@ func (s *userService) Create(ctx context.Context, user *userv3.User) (*userv3.Us } user.Spec.RecoveryUrl = &rl + CreateUserAuditEvent(ctx, s.db, AuditActionCreate, user.GetMetadata().GetName(), uid, []uuid.UUID{}, rolesAfter) return user, nil } @@ -452,10 +455,10 @@ func (s *userService) GetByName(ctx context.Context, user *userv3.User) (*userv3 } func (s *userService) GetUserInfo(ctx context.Context, user *userv3.User) (*userv3.UserInfo, error) { - sd, ok := ctx.Value(common.SessionDataKey).(*commonv3.SessionData) + sd, ok := GetSessionDataFromContext(ctx) username := "" if !ok { - return &userv3.UserInfo{}, fmt.Errorf("cannot perform project listing without auth") + return &userv3.UserInfo{}, fmt.Errorf("cannot get user info without auth") } username = sd.Username @@ -517,26 +520,42 @@ func (s *userService) GetUserInfo(ctx context.Context, user *userv3.User) (*user return &userv3.UserInfo{}, fmt.Errorf("unable to get user info") } -func (s *userService) deleteUserRoleRelations(ctx context.Context, db bun.IDB, userId uuid.UUID, user *userv3.User) error { - err := dao.DeleteX(ctx, db, "account_id", userId, &models.AccountResourcerole{}) +func (s *userService) deleteUserRoleRelations(ctx context.Context, db bun.IDB, userId uuid.UUID, user *userv3.User) ([]uuid.UUID, error) { + ids := []uuid.UUID{} + + ar := []models.AccountResourcerole{} + err := dao.DeleteXR(ctx, db, "account_id", userId, &ar) if err != nil { - return err + return nil, err } - err = dao.DeleteX(ctx, db, "account_id", userId, &models.ProjectAccountResourcerole{}) - if err != nil { - return err + for _, r := range ar { + ids = append(ids, r.RoleId) } - err = dao.DeleteX(ctx, db, "account_id", userId, &models.ProjectAccountNamespaceRole{}) + + par := []models.ProjectAccountResourcerole{} + err = dao.DeleteX(ctx, db, "account_id", userId, &par) if err != nil { - return err + return nil, err + } + for _, r := range par { + ids = append(ids, r.RoleId) + } + + panr := []models.ProjectAccountNamespaceRole{} + err = dao.DeleteX(ctx, db, "account_id", userId, &panr) + if err != nil { + return nil, err + } + for _, r := range panr { + ids = append(ids, r.RoleId) } _, err = s.azc.DeletePolicies(ctx, &authzv1.Policy{Sub: "u:" + user.GetMetadata().GetName()}) if err != nil { - return fmt.Errorf("unable to delete user-role relations from authz; %v", err) + return nil, fmt.Errorf("unable to delete user-role relations from authz; %v", err) } - return nil + return ids, nil } func (s *userService) Update(ctx context.Context, user *userv3.User) (*userv3.User, error) { @@ -566,7 +585,7 @@ func (s *userService) Update(ctx context.Context, user *userv3.User) (*userv3.Us return &userv3.User{}, err } - err = s.deleteUserRoleRelations(ctx, tx, usr.ID, user) + rolesBefore, err := s.deleteUserRoleRelations(ctx, tx, usr.ID, user) if err != nil { tx.Rollback() return &userv3.User{}, err @@ -578,7 +597,8 @@ func (s *userService) Update(ctx context.Context, user *userv3.User) (*userv3.Us return &userv3.User{}, err } - user, err = s.createUserRoleRelations(ctx, tx, user, parsedIds{Id: usr.ID, Partner: partnerId, Organization: organizationId}) + // TODO: add user-group relations to audit + user, rolesAfter, err := s.createUserRoleRelations(ctx, tx, user, parsedIds{Id: usr.ID, Partner: partnerId, Organization: organizationId}) if err != nil { tx.Rollback() return &userv3.User{}, err @@ -595,6 +615,8 @@ func (s *userService) Update(ctx context.Context, user *userv3.User) (*userv3.Us tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateUserAuditEvent(ctx, s.db, AuditActionUpdate, user.GetMetadata().GetName(), usr.ID, rolesBefore, rolesAfter) return user, nil } else { @@ -617,13 +639,7 @@ func (s *userService) Delete(ctx context.Context, user *userv3.User) (*userrpcv3 return &userrpcv3.DeleteUserResponse{}, err } - err = s.deleteUserRoleRelations(ctx, tx, usr.ID, user) - if err != nil { - tx.Rollback() - return &userrpcv3.DeleteUserResponse{}, err - } - - err = s.ap.Delete(ctx, usr.ID.String()) + rolesBefore, err := s.deleteUserRoleRelations(ctx, tx, usr.ID, user) if err != nil { tx.Rollback() return &userrpcv3.DeleteUserResponse{}, err @@ -635,11 +651,19 @@ func (s *userService) Delete(ctx context.Context, user *userv3.User) (*userrpcv3 return &userrpcv3.DeleteUserResponse{}, fmt.Errorf("unable to delete user; %v", err) } + err = s.ap.Delete(ctx, usr.ID.String()) + if err != nil { + tx.Rollback() + return &userrpcv3.DeleteUserResponse{}, err + } + err = tx.Commit() if err != nil { tx.Rollback() _log.Warn("unable to commit changes", err) } + + CreateUserAuditEvent(ctx, s.db, AuditActionDelete, user.GetMetadata().GetName(), usr.ID, rolesBefore, []uuid.UUID{}) return &userrpcv3.DeleteUserResponse{}, nil } return &userrpcv3.DeleteUserResponse{}, fmt.Errorf("unable to delete user '%v'", user.Metadata.Name) diff --git a/pkg/service/utils.go b/pkg/service/utils.go index a293a62..da85967 100644 --- a/pkg/service/utils.go +++ b/pkg/service/utils.go @@ -4,6 +4,8 @@ import ( "context" "github.com/RafayLabs/rcloud-base/internal/dao" + "github.com/RafayLabs/rcloud-base/pkg/common" + commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3" "github.com/google/uuid" "github.com/uptrace/bun" ) @@ -29,6 +31,15 @@ func contains(s []string, str string) bool { return false } +func containsu(s []uuid.UUID, id uuid.UUID) bool { + for _, v := range s { + if v == id { + return true + } + } + return false +} + func remove(l []string, item string) []string { for i, other := range l { if other == item { @@ -38,6 +49,47 @@ func remove(l []string, item string) []string { return l } +func diff(before, after []string) ([]string, []string, []string) { + cu := []string{} + uu := []string{} + du := []string{} + + for _, u := range after { + if contains(before, u) { + uu = append(uu, u) + } else { + cu = append(du, u) + } + } + for _, u := range before { + if !contains(uu, u) && !contains(du, u) { + du = append(cu, u) + } + } + return cu, uu, du +} + +// Given two lists, return newly created, unchanged and deleted items +func diffu(before, after []uuid.UUID) ([]uuid.UUID, []uuid.UUID, []uuid.UUID) { + cu := []uuid.UUID{} + uu := []uuid.UUID{} + du := []uuid.UUID{} + + for _, u := range after { + if containsu(before, u) { + uu = append(uu, u) + } else { + cu = append(du, u) + } + } + for _, u := range before { + if !containsu(uu, u) && !containsu(du, u) { + du = append(cu, u) + } + } + return cu, uu, du +} + func getPartnerOrganization(ctx context.Context, db bun.IDB, partner, org string) (uuid.UUID, uuid.UUID, error) { partnerId, err := dao.GetPartnerId(ctx, db, partner) if err != nil { @@ -50,3 +102,8 @@ func getPartnerOrganization(ctx context.Context, db bun.IDB, partner, org string return partnerId, organizationId, nil } + +func GetSessionDataFromContext(ctx context.Context) (*commonv3.SessionData, bool) { + s, ok := ctx.Value(common.SessionDataKey).(*commonv3.SessionData) + return s, ok +} diff --git a/server/user.go b/server/user.go index 6fb4164..89e41ae 100644 --- a/server/user.go +++ b/server/user.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" - "github.com/RafayLabs/rcloud-base/pkg/common" "github.com/RafayLabs/rcloud-base/pkg/query" "github.com/RafayLabs/rcloud-base/pkg/service" rpcv3 "github.com/RafayLabs/rcloud-base/proto/rpc/user" @@ -75,7 +74,7 @@ func (s *userServer) UpdateUser(ctx context.Context, req *userpbv3.User) (*userp } func (s *userServer) DownloadCliConfig(ctx context.Context, req *rpcv3.CliConfigRequest) (*commonv3.HttpBody, error) { - sessData, ok := ctx.Value(common.SessionDataKey).(*commonv3.SessionData) + sessData, ok := service.GetSessionDataFromContext(ctx) if !ok { return nil, fmt.Errorf("unable to retrieve session data") }