mirror of
https://github.com/paralus/paralus.git
synced 2026-08-24 15:47:19 +00:00
Add basic audit logging setup
This commit is contained in:
@@ -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().
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+27
@@ -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
|
||||
|
||||
+54
-21
@@ -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
|
||||
}
|
||||
|
||||
+11
-1
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
+73
-42
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
+16
-9
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+58
-34
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-2
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user