From d591ab0d11bcb72897291a6391ba3a37580d2f69 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Tue, 21 Jan 2025 14:18:54 +0200 Subject: [PATCH 1/5] add audit logs --- server/app/app.go | 5 ++- server/app/audit_handler.go | 44 +++++++++++++++++++++ server/docs/docs.go | 73 +++++++++++++++++++++++++++++++++++ server/docs/swagger.yaml | 48 +++++++++++++++++++++++ server/go.mod | 1 + server/go.sum | 2 + server/middlewares/logging.go | 33 +++++++++++++--- server/models/audit_log.go | 26 +++++++++++++ server/models/database.go | 1 + 9 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 server/app/audit_handler.go create mode 100644 server/models/audit_log.go diff --git a/server/app/app.go b/server/app/app.go index 85175215..98352582 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -118,6 +118,7 @@ func (a *App) registerHandlers() { userRouter := authRouter.PathPrefix("/user").Subrouter() invoiceRouter := authRouter.PathPrefix("/invoice").Subrouter() cardRouter := userRouter.PathPrefix("/card").Subrouter() + logRouter := userRouter.PathPrefix("/log").Subrouter() notificationRouter := authRouter.PathPrefix("/notification").Subrouter() vmRouter := authRouter.PathPrefix("/vm").Subrouter() k8sRouter := authRouter.PathPrefix("/k8s").Subrouter() @@ -156,6 +157,8 @@ func (a *App) registerHandlers() { cardRouter.HandleFunc("", WrapFunc(a.ListCardHandler)).Methods("GET", "OPTIONS") cardRouter.HandleFunc("/default", WrapFunc(a.SetDefaultCardHandler)).Methods("PUT", "OPTIONS") + logRouter.HandleFunc("", WrapFunc(a.ListLogsHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS") invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS") invoiceRouter.HandleFunc("/download/{id}", WrapFunc(a.DownloadInvoiceHandler)).Methods("GET", "OPTIONS") @@ -204,10 +207,10 @@ func (a *App) registerHandlers() { voucherRouter.HandleFunc("/all/reset", WrapFunc(a.ResetUsersVoucherBalanceHandler)).Methods("PUT", "OPTIONS") // middlewares - r.Use(middlewares.LoggingMW) r.Use(middlewares.EnableCors) authRouter.Use(middlewares.Authorization(a.db, a.config.Token.Secret, a.config.Token.Timeout)) + authRouter.Use(middlewares.AuditLogMiddleware(a.db)) adminRouter.Use(middlewares.AdminAccess(a.db)) // prometheus registration diff --git a/server/app/audit_handler.go b/server/app/audit_handler.go new file mode 100644 index 00000000..3145d75c --- /dev/null +++ b/server/app/audit_handler.go @@ -0,0 +1,44 @@ +package app + +import ( + "errors" + "net/http" + + "github.com/codescalers/cloud4students/middlewares" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +// Example endpoint: List user's logs +// @Summary List user's logs +// @Description List user's logs +// @Tags Audit +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.AuditLog +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/log [get] +func (a *App) ListLogsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + logs, err := a.db.GetUserLogs(userID) + if err == gorm.ErrRecordNotFound || len(logs) == 0 { + return ResponseMsg{ + Message: "no logs found", + Data: logs, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Logs are found", + Data: logs, + }, Ok() +} diff --git a/server/docs/docs.go b/server/docs/docs.go index 4a370f39..8c6152ca 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -1937,6 +1937,53 @@ const docTemplate = `{ } } }, + "/user/log": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's logs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List user's logs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditLog" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/user/refresh_token": { "post": { "security": [ @@ -3095,6 +3142,32 @@ const docTemplate = `{ "voucherAndBalanceAndCard" ] }, + "models.AuditLog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "models.Card": { "type": "object", "required": [ diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 62bbb041..89f97757 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -302,6 +302,23 @@ definitions: - voucherAndCard - balanceAndCard - voucherAndBalanceAndCard + models.AuditLog: + properties: + id: + type: integer + method: + type: string + status_code: + type: integer + success: + type: boolean + timestamp: + type: string + url: + type: string + user_id: + type: string + type: object models.Card: properties: brand: @@ -1918,6 +1935,37 @@ paths: summary: Send code to forget password email for verification tags: - User + /user/log: + get: + consumes: + - application/json + description: List user's logs + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.AuditLog' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List user's logs + tags: + - Audit /user/refresh_token: post: consumes: diff --git a/server/go.mod b/server/go.mod index 694342ee..c77dac94 100644 --- a/server/go.mod +++ b/server/go.mod @@ -23,6 +23,7 @@ require ( github.com/swaggo/swag v1.16.4 github.com/threefoldtech/tfgrid-sdk-go/grid-client v0.16.0 github.com/threefoldtech/tfgrid-sdk-go/grid-proxy v0.16.0 + github.com/urfave/negroni/v3 v3.1.1 golang.org/x/crypto v0.30.0 golang.org/x/text v0.21.0 gopkg.in/validator.v2 v2.0.1 diff --git a/server/go.sum b/server/go.sum index 1a3e3b89..0757d794 100644 --- a/server/go.sum +++ b/server/go.sum @@ -204,6 +204,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= +github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= github.com/vedhavyas/go-subkey v1.0.3 h1:iKR33BB/akKmcR2PMlXPBeeODjWLM90EL98OrOGs8CA= github.com/vedhavyas/go-subkey v1.0.3/go.mod h1:CloUaFQSSTdWnINfBRFjVMkWXZANW+nd8+TI5jYcl6Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/server/middlewares/logging.go b/server/middlewares/logging.go index 29f2fbbd..9f1829f8 100644 --- a/server/middlewares/logging.go +++ b/server/middlewares/logging.go @@ -3,14 +3,35 @@ package middlewares import ( "net/http" + "time" + "github.com/codescalers/cloud4students/models" "github.com/rs/zerolog/log" + "github.com/urfave/negroni/v3" ) -// LoggingMW logs all information of every request -func LoggingMW(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Info().Timestamp().Str("method", r.Method).Str("uri", r.RequestURI).Send() - h.ServeHTTP(w, r) - }) +func AuditLogMiddleware(db models.DB) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userID := r.Context().Value(UserIDKey("UserID")).(string) + + lrw := negroni.NewResponseWriter(w) + h.ServeHTTP(lrw, r) + + statusCode := lrw.Status() + + l := models.AuditLog{ + UserID: userID, + Method: r.Method, + URL: r.URL.Path, + Timestamp: time.Now(), + Success: statusCode >= 200 && statusCode < 300, + StatusCode: statusCode, + } + + if err := db.CreateAuditLog(&l); err != nil { + log.Error().Err(err).Msg("logging audit failed") + } + }) + } } diff --git a/server/models/audit_log.go b/server/models/audit_log.go new file mode 100644 index 00000000..8c5758c0 --- /dev/null +++ b/server/models/audit_log.go @@ -0,0 +1,26 @@ +package models + +import ( + "time" +) + +// Struct to represent audit log details +type AuditLog struct { + ID uint `gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"not null"` + Method string `json:"method"` + URL string `json:"url"` + Timestamp time.Time `json:"timestamp"` + Success bool `json:"success"` + StatusCode int `json:"status_code"` +} + +func (d *DB) CreateAuditLog(l *AuditLog) error { + return d.db.Create(&l).Error +} + +// GetUserLogs gets user logs +func (d *DB) GetUserLogs(userID string) ([]AuditLog, error) { + var res []AuditLog + return res, d.db.Find(&res, "user_id = ?", userID).Error +} diff --git a/server/models/database.go b/server/models/database.go index c0b0fc4e..49f722e9 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -32,6 +32,7 @@ func (d *DB) Migrate() error { err := d.db.AutoMigrate( &User{}, &State{}, &Card{}, &Invoice{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}, &DeploymentItem{}, &PaymentDetails{}, + AuditLog{}, ) if err != nil { return err From ecfafd19597a60ad9328817fc1826cd46c883794 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Tue, 21 Jan 2025 16:54:36 +0200 Subject: [PATCH 2/5] add audit events --- server/app/admin_handler.go | 35 ++ server/app/app.go | 3 + server/app/audit_handler.go | 34 ++ server/app/events.go | 552 +++++++++++++++++++++++ server/app/invoice_handler.go | 37 ++ server/app/k8s_handler.go | 10 + server/app/notification_handler.go | 7 + server/app/payments_handler.go | 25 + server/app/user_handler.go | 77 ++++ server/app/vm_handler.go | 10 + server/app/voucher_handler.go | 20 + server/deployer/deployer.go | 32 ++ server/deployer/k8s_deployer.go | 5 + server/deployer/vms_deployer.go | 4 + server/docs/docs.go | 70 +++ server/docs/swagger.yaml | 46 ++ server/models/{audit_log.go => audit.go} | 19 +- server/models/database.go | 2 +- 18 files changed, 986 insertions(+), 2 deletions(-) create mode 100644 server/app/events.go rename server/models/{audit_log.go => audit.go} (54%) diff --git a/server/app/admin_handler.go b/server/app/admin_handler.go index 0032eb03..95d04ffa 100644 --- a/server/app/admin_handler.go +++ b/server/app/admin_handler.go @@ -169,6 +169,11 @@ func (a *App) SetPricesHandler(req *http.Request) (interface{}, Response) { a.config.PricesPerMonth.PublicIP = input.PublicIP } + if err := a.logVMsPriceUpdate(req, a.config.PricesPerMonth); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "New prices are set", Data: nil, @@ -314,6 +319,11 @@ func (a *App) DeleteAllDeploymentsHandler(req *http.Request) (interface{}, Respo } } + if err := a.logAllDeploymentsDelete(req); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Deployments are deleted successfully", }, Ok() @@ -416,6 +426,11 @@ func (a *App) UpdateMaintenanceHandler(req *http.Request) (interface{}, Response return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logMaintenanceUpdate(req, input.ON); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Maintenance is updated successfully", Data: nil, @@ -477,6 +492,11 @@ func (a *App) SetAdminHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logAdminSet(req, user.ID.String(), input.Admin); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is updated successfully", }, Ok() @@ -537,6 +557,11 @@ func (a *App) CreateNewAnnouncementHandler(req *http.Request) (interface{}, Resp } } + if err := a.logAnnouncementCreate(req, adminAnnouncement.Subject); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "new announcement is sent successfully", }, Created() @@ -598,6 +623,11 @@ func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logEmailSent(req, user.ID.String(), emailUser.Subject); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "new email is sent successfully", }, Created() @@ -636,6 +666,11 @@ func (a *App) UpdateNextLaunchHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logNextLaunchUpdate(req, input.Launched); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Next Launch is updated successfully", Data: nil, diff --git a/server/app/app.go b/server/app/app.go index 98352582..690fa19f 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -119,6 +119,7 @@ func (a *App) registerHandlers() { invoiceRouter := authRouter.PathPrefix("/invoice").Subrouter() cardRouter := userRouter.PathPrefix("/card").Subrouter() logRouter := userRouter.PathPrefix("/log").Subrouter() + eventRouter := userRouter.PathPrefix("/event").Subrouter() notificationRouter := authRouter.PathPrefix("/notification").Subrouter() vmRouter := authRouter.PathPrefix("/vm").Subrouter() k8sRouter := authRouter.PathPrefix("/k8s").Subrouter() @@ -159,6 +160,8 @@ func (a *App) registerHandlers() { logRouter.HandleFunc("", WrapFunc(a.ListLogsHandler)).Methods("GET", "OPTIONS") + eventRouter.HandleFunc("", WrapFunc(a.ListEventsHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS") invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS") invoiceRouter.HandleFunc("/download/{id}", WrapFunc(a.DownloadInvoiceHandler)).Methods("GET", "OPTIONS") diff --git a/server/app/audit_handler.go b/server/app/audit_handler.go index 3145d75c..6c49ef83 100644 --- a/server/app/audit_handler.go +++ b/server/app/audit_handler.go @@ -42,3 +42,37 @@ func (a *App) ListLogsHandler(req *http.Request) (interface{}, Response) { Data: logs, }, Ok() } + +// Example endpoint: List user's events +// @Summary List user's events +// @Description List user's events +// @Tags Audit +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.AuditEvent +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/event [get] +func (a *App) ListEventsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + events, err := a.db.GetUserEvents(userID) + if err == gorm.ErrRecordNotFound || len(events) == 0 { + return ResponseMsg{ + Message: "no events found", + Data: events, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Events are found", + Data: events, + }, Ok() +} diff --git a/server/app/events.go b/server/app/events.go new file mode 100644 index 00000000..1cafe8f2 --- /dev/null +++ b/server/app/events.go @@ -0,0 +1,552 @@ +package app + +import ( + "fmt" + "net/http" + "time" + + "github.com/codescalers/cloud4students/internal" + "github.com/codescalers/cloud4students/middlewares" + "github.com/codescalers/cloud4students/models" + "github.com/pkg/errors" +) + +type role string + +var ( + userRole role = "User" + adminRole role = "Admin" + systemRole role = "System" +) + +func (a *App) logUserDelete(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is deleted", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logBalanceCharge(userID, currency string, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_balance", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Balance is charged with %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserVoucherActivate(userID, currency, voucher string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "apply_voucher", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User activated a voucher %v with %v %v", voucher, balance, currency), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserVoucherApply(userID, currency string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "apply_voucher", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User applied for a voucher with %v %v", balance, currency), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserUpdate(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: "User data is updated successfully", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserPasswordUpdate(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_user_password", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: "Password is updated successfully", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserSignedIn(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "signin_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is signed in successfully", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserCreated(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is created", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherReset(userID string, voucherBalance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "reset_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher balance %v is reset", voucherBalance), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherUpdate(userID, voucher string, balance uint64, approved bool) error { + state := "Approved" + if approved { + state = "Rejected" + } + + event := models.AuditEvent{ + UserID: userID, + Action: "update_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher `%v` with balance %v, is %v", voucher, balance, state), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherCreate(userID, voucher string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher `%v` with balance %v, is created successfully", voucher, balance), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardDelete(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is deleted", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardDefaultSet(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "set_default_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is set as default", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardAdded(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "add_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is added", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logNotificationSeen(userID string, notificationID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "seen_notification", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Notification %v is seen", notificationID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherBalanceUpdate(userID, currency string, role role, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_voucher_balance", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Voucher balance is updated to %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logBalanceUpdate(userID, currency string, role role, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_balance", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Balance is updated to %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logK8sDelete(userID string, role role, k8sID int, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_k8s", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Kubernetes %v which created at %v, is deleted", k8sID, createdAt, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVMDelete(userID string, role role, vmID int, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_vm", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Virtual machine %v which created at %v, is deleted", + vmID, createdAt, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoiceCreate(userID, currency string, invoiceID int, invoiceTotal float64, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_invoice", + Role: string(systemRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Invoice %v with value: %v %v is created at %v", + invoiceID, invoiceTotal, currency, createdAt, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoicePayment(userID, currency string, invoiceTotal float64, paymentDetails models.PaymentDetails) error { + event := models.AuditEvent{ + UserID: userID, + Action: "pay_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Invoice %v with value: %v %v is paid using: {balance: %v, vouchers: %v, card:%v}", + paymentDetails.InvoiceID, invoiceTotal, currency, + paymentDetails.Balance, paymentDetails.VoucherBalance, paymentDetails.Card, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoicePDFUpdate(req *http.Request, invoiceID int) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Invoice %v pdf data is updated", invoiceID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoiceDownload(req *http.Request, invoiceID int) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "download_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Invoice %v is downloaded", invoiceID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logEmailSent(req *http.Request, targetUserID, subject string) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "send_email", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("An email is sent to: %v, with subject: %v", targetUserID, subject), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAnnouncementCreate(req *http.Request, subject string) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "create_announcement", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("An announcement is created with subject: %v", subject), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAdminSet(req *http.Request, adminID string, admin bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + metaData := fmt.Sprintf("A new admin %v is added", adminID) + if !admin { + metaData = fmt.Sprintf("An admin %v is removed", adminID) + } + + event := models.AuditEvent{ + UserID: userID, + Action: "set_admin", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: metaData, + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logNextLaunchUpdate(req *http.Request, on bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_next_launch", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Next launch value is updated to: %v", on), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logMaintenanceUpdate(req *http.Request, on bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_maintenance", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Maintenance value is updated to: %v", on), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAllDeploymentsDelete(req *http.Request) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "delete_all_deployments", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: "All virtual machines are deleted", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVMsPriceUpdate(req *http.Request, prices internal.Prices) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_vms_prices", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Virtual machines prices are updated {small: %v, medium: %v, large: %v, public IPs: %v}", + prices.SmallVM, prices.MediumVM, prices.LargeVM, prices.PublicIP, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} diff --git a/server/app/invoice_handler.go b/server/app/invoice_handler.go index 362fb2fb..2a96c4e6 100644 --- a/server/app/invoice_handler.go +++ b/server/app/invoice_handler.go @@ -174,12 +174,22 @@ func (a *App) DownloadInvoiceHandler(req *http.Request) (interface{}, Response) log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logInvoicePDFUpdate(req, invoice.ID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } if userID != invoice.UserID { return nil, NotFound(errors.New("invoice is not found")) } + if err := a.logInvoiceDownload(req, invoice.ID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return invoice.FileData, Ok(). WithHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID))). WithHeader("Content-Type", "application/pdf") @@ -430,6 +440,10 @@ func (a *App) createInvoice(user models.User, now time.Time) error { if err = a.db.CreateInvoice(&in); err != nil { return err } + + if err := a.logInvoiceCreate(user.ID.String(), a.config.Currency, in.ID, in.Total, in.CreatedAt); err != nil { + return err + } } return nil @@ -451,12 +465,20 @@ func (a *App) deleteInvoiceDeploymentsNotPaidSince3Months(userID string, now tim if err = a.db.DeleteVMByID(dl.DeploymentID); err != nil { log.Error().Err(err).Send() } + + if err := a.logVMDelete(userID, systemRole, dl.DeploymentID, dl.DeploymentCreatedAt); err != nil { + log.Error().Err(err).Send() + } } if dl.DeploymentType == "k8s" { if err = a.db.DeleteK8s(dl.DeploymentID); err != nil { log.Error().Err(err).Send() } + + if err := a.logK8sDelete(userID, systemRole, dl.DeploymentID, dl.DeploymentCreatedAt); err != nil { + log.Error().Err(err).Send() + } } } } @@ -669,6 +691,11 @@ func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, log.Error().Err(err).Send() return InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherBalanceUpdate(user.ID.String(), a.config.Currency, systemRole, user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } } // invoice used balance @@ -677,6 +704,11 @@ func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, log.Error().Err(err).Send() return InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logBalanceUpdate(user.ID.String(), a.config.Currency, systemRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } } paymentDetails.InvoiceID = invoiceID @@ -689,6 +721,11 @@ func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, return InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logInvoicePayment(user.ID.String(), a.config.Currency, invoiceTotal, paymentDetails); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } + return nil } diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go index 70f59942..ea2c4a51 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -324,6 +324,11 @@ func (a *App) K8sDeleteHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logK8sDelete(userID, userRole, cluster.ID, cluster.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // metrics middlewares.Deletions.WithLabelValues(userID, "k8s").Inc() @@ -377,6 +382,11 @@ func (a *App) K8sDeleteAllHandler(req *http.Request) (interface{}, Response) { } for _, c := range clusters { + if err := a.logK8sDelete(userID, userRole, c.ID, c.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(c.UserID, "k8s").Inc() } diff --git a/server/app/notification_handler.go b/server/app/notification_handler.go index c3e7dfc9..0b51f276 100644 --- a/server/app/notification_handler.go +++ b/server/app/notification_handler.go @@ -62,6 +62,8 @@ func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response // @Failure 500 {object} Response // @Router /notification/{id} [put] func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + id, err := strconv.Atoi(mux.Vars(req)["id"]) if err != nil { log.Error().Err(err).Send() @@ -74,6 +76,11 @@ func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Respon return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logNotificationSeen(userID, id); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Notifications are updated", Data: nil, diff --git a/server/app/payments_handler.go b/server/app/payments_handler.go index 9aaa84c8..1481e6c7 100644 --- a/server/app/payments_handler.go +++ b/server/app/payments_handler.go @@ -128,6 +128,11 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logCardAdded(userID, paymentMethod.Card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // if no payment is added before then we update the user payment ID with it as a default if len(strings.TrimSpace(user.StripeDefaultPaymentID)) == 0 { // Update the default payment method for future payments @@ -146,6 +151,11 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logCardDefaultSet(userID, paymentMethod.Card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } // try to settle old invoices using the card @@ -229,6 +239,11 @@ func (a *App) SetDefaultCardHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logCardDefaultSet(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Card is set as default successfully", Data: nil, @@ -378,6 +393,11 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logCardDefaultSet(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } // If user has another cards or no active deployments, so can delete @@ -392,6 +412,11 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logCardDelete(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Card is deleted successfully", Data: nil, diff --git a/server/app/user_handler.go b/server/app/user_handler.go index ff0d0f1b..94a3c19f 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -251,6 +251,11 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserCreated(user.ID.String()); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Account is created successfully.", }, Created() @@ -302,6 +307,11 @@ func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserSignedIn(user.ID.String()); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "You are signed in successfully", Data: AccessTokenResponse{Token: token, Timeout: a.config.Token.Timeout}, @@ -502,6 +512,8 @@ func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, R // @Failure 500 {object} Response // @Router /user/change_password [put] func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + var data ChangePasswordInput err := json.NewDecoder(req.Body).Decode(&data) if err != nil { @@ -534,6 +546,11 @@ func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserPasswordUpdate(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Password is updated successfully", Data: nil, @@ -633,6 +650,11 @@ func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserUpdate(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is updated successfully", }, Ok() @@ -722,6 +744,11 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) } middlewares.VoucherApplied.WithLabelValues(userID, voucher.Voucher, fmt.Sprint(voucher.Balance)).Inc() + if err := a.logUserVoucherApply(userID, a.config.Currency, input.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Voucher request is being reviewed, you'll receive a confirmation mail soon", Data: nil, @@ -805,18 +832,33 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) } } + if err := a.logUserVoucherActivate(userID, a.config.Currency, voucherBalance.Voucher, voucherBalance.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherBalanceUpdate(userID, a.config.Currency, userRole, user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + err = a.db.UpdateUserBalance(user.ID.String(), user.Balance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.VoucherActivated.WithLabelValues(userID, voucherBalance.Voucher, fmt.Sprint(voucherBalance.Balance)).Inc() return ResponseMsg{ @@ -872,6 +914,11 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { user.Balance += float64(input.Amount) + if err := a.logBalanceCharge(userID, a.config.Currency, input.Amount); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // try to settle old invoices invoices, err := a.db.ListUnpaidInvoices(user.ID.String()) if err != nil { @@ -892,12 +939,22 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Balance is charged successfully", Data: clientSecretResponse{ClientSecret: intent.ClientSecret}, @@ -996,6 +1053,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } err = a.db.DeleteAllVms(userID) @@ -1017,6 +1079,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logK8sDelete(userID, userRole, cluster.ID, cluster.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } if len(clusters) > 0 { @@ -1040,6 +1107,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logCardDelete(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } err = a.db.DeleteAllCards(userID) @@ -1060,6 +1132,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserDelete(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is deleted successfully", }, Ok() diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index 551293f0..5d894c4f 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -294,6 +294,11 @@ func (a *App) DeleteVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(userID, "vms").Inc() return ResponseMsg{ Message: "Virtual machine is deleted successfully", @@ -345,6 +350,11 @@ func (a *App) DeleteAllVMsHandler(req *http.Request) (interface{}, Response) { // metrics for _, vm := range vms { + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(vm.UserID, "vms").Inc() } diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index eb6a58fb..92051728 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -68,6 +68,11 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherCreate(v.UserID, v.Voucher, v.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Voucher is generated successfully", Data: map[string]string{"voucher": voucher}, @@ -182,6 +187,11 @@ func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherUpdate(voucher.UserID, voucher.Voucher, voucher.Balance, input.Approved); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Update mail has been sent to the user", Data: nil, @@ -233,6 +243,11 @@ func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Respons log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherUpdate(v.UserID, v.Voucher, v.Balance, true); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } return ResponseMsg{ @@ -269,6 +284,11 @@ func (a *App) ResetUsersVoucherBalanceHandler(req *http.Request) (interface{}, R log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherReset(user.ID.String(), user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } return ResponseMsg{ diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index 1410b3dd..51d9b43c 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -333,3 +333,35 @@ func UsagePercentageInMonth(start time.Time, end time.Time) (float64, error) { return usedHours / totalHoursInMonth, nil } + +func logVMCreate(db models.DB, userID string, vmID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_vm", + Role: "User", + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Virtual machine %v is created successfully", vmID), + } + + if err := db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func logK8sCreate(db models.DB, userID string, k8sID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_k8s", + Role: "User", + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Kubernetes %v is created successfully", k8sID), + } + + if err := db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index 08abd22a..ab6fb980 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -170,6 +170,11 @@ func (d *Deployer) loadK8s( return models.K8sCluster{}, err } + if err := logK8sCreate(d.db, k8s.UserID, k8s.ID); err != nil { + log.Error().Err(err).Send() + return models.K8sCluster{}, err + } + return k8s, nil } diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index 37970bdc..8cff1a91 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -137,6 +137,10 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, vm mod return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } + if err := logVMCreate(d.db, vm.UserID, vm.ID); err != nil { + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) + } + middlewares.Deployments.WithLabelValues(user.ID.String(), vm.Resources, "vm").Inc() return 0, nil, nil } diff --git a/server/docs/docs.go b/server/docs/docs.go index 8c6152ca..7ce12cc7 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -1837,6 +1837,53 @@ const docTemplate = `{ } } }, + "/user/event": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List user's events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditEvent" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/user/forget_password/verify_email": { "post": { "description": "Verify user's email to reset password", @@ -3142,6 +3189,29 @@ const docTemplate = `{ "voucherAndBalanceAndCard" ] }, + "models.AuditEvent": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "metadata": { + "type": "string" + }, + "role": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "models.AuditLog": { "type": "object", "properties": { diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 89f97757..4f74f525 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -302,6 +302,21 @@ definitions: - voucherAndCard - balanceAndCard - voucherAndBalanceAndCard + models.AuditEvent: + properties: + action: + type: string + id: + type: integer + metadata: + type: string + role: + type: string + timestamp: + type: string + user_id: + type: string + type: object models.AuditLog: properties: id: @@ -1867,6 +1882,37 @@ paths: summary: Charge user balance tags: - User + /user/event: + get: + consumes: + - application/json + description: List user's events + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.AuditEvent' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List user's events + tags: + - Audit /user/forget_password/verify_email: post: consumes: diff --git a/server/models/audit_log.go b/server/models/audit.go similarity index 54% rename from server/models/audit_log.go rename to server/models/audit.go index 8c5758c0..89c8c3ae 100644 --- a/server/models/audit_log.go +++ b/server/models/audit.go @@ -19,8 +19,25 @@ func (d *DB) CreateAuditLog(l *AuditLog) error { return d.db.Create(&l).Error } -// GetUserLogs gets user logs func (d *DB) GetUserLogs(userID string) ([]AuditLog, error) { var res []AuditLog return res, d.db.Find(&res, "user_id = ?", userID).Error } + +type AuditEvent struct { + ID uint `gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"not null"` + Action string `json:"action"` + Role string `json:"role"` + Timestamp time.Time `json:"timestamp"` + Metadata string `json:"metadata"` +} + +func (d *DB) CreateAuditEvent(e *AuditEvent) error { + return d.db.Create(&e).Error +} + +func (d *DB) GetUserEvents(userID string) ([]AuditEvent, error) { + var res []AuditEvent + return res, d.db.Find(&res, "user_id = ?", userID).Error +} diff --git a/server/models/database.go b/server/models/database.go index 49f722e9..d376a1bf 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -32,7 +32,7 @@ func (d *DB) Migrate() error { err := d.db.AutoMigrate( &User{}, &State{}, &Card{}, &Invoice{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}, &DeploymentItem{}, &PaymentDetails{}, - AuditLog{}, + AuditLog{}, AuditEvent{}, ) if err != nil { return err From e2919d0ec7d6677c46e41b0877a5f1e213c769ce Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Wed, 12 Feb 2025 12:07:56 +0200 Subject: [PATCH 3/5] fixed balance in vouchers --- server/app/user_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 0c4fd2ba..93750aea 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -743,7 +743,7 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) } middlewares.VoucherApplied.WithLabelValues(userID, voucher.Voucher, fmt.Sprint(voucher.Balance)).Inc() - if err := a.logUserVoucherApply(userID, a.config.Currency, input.Balance); err != nil { + if err := a.logUserVoucherApply(userID, a.config.Currency, a.config.VoucherBalance); err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } From 4a981192fc4e5cfd3f18725fc10c00568bbc1eba Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Wed, 5 Mar 2025 14:49:10 +0200 Subject: [PATCH 4/5] remove seen notifications from audit --- server/app/events.go | 16 ---------------- server/app/notification_handler.go | 5 ----- 2 files changed, 21 deletions(-) diff --git a/server/app/events.go b/server/app/events.go index 1cafe8f2..1a1b3ae7 100644 --- a/server/app/events.go +++ b/server/app/events.go @@ -251,22 +251,6 @@ func (a *App) logCardAdded(userID, last4digits string) error { return nil } -func (a *App) logNotificationSeen(userID string, notificationID int) error { - event := models.AuditEvent{ - UserID: userID, - Action: "seen_notification", - Role: string(userRole), - Timestamp: time.Now(), - Metadata: fmt.Sprintf("Notification %v is seen", notificationID), - } - - if err := a.db.CreateAuditEvent(&event); err != nil { - return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) - } - - return nil -} - func (a *App) logVoucherBalanceUpdate(userID, currency string, role role, balance float64) error { event := models.AuditEvent{ UserID: userID, diff --git a/server/app/notification_handler.go b/server/app/notification_handler.go index 0b51f276..d121a758 100644 --- a/server/app/notification_handler.go +++ b/server/app/notification_handler.go @@ -76,11 +76,6 @@ func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Respon return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - if err := a.logNotificationSeen(userID, id); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - return ResponseMsg{ Message: "Notifications are updated", Data: nil, From 7e1971eefad6de053c5a18a89d600a78ef4efff7 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Wed, 5 Mar 2025 14:57:08 +0200 Subject: [PATCH 5/5] format date in audit metadata --- server/app/events.go | 6 +++--- server/app/notification_handler.go | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/server/app/events.go b/server/app/events.go index 1a1b3ae7..c8e6123a 100644 --- a/server/app/events.go +++ b/server/app/events.go @@ -296,7 +296,7 @@ func (a *App) logK8sDelete(userID string, role role, k8sID int, createdAt time.T Role: string(role), Timestamp: time.Now(), Metadata: fmt.Sprintf( - "Kubernetes %v which created at %v, is deleted", k8sID, createdAt, + "Kubernetes %v which created at %v, is deleted", k8sID, createdAt.Format("January 2, 2006"), ), } @@ -315,7 +315,7 @@ func (a *App) logVMDelete(userID string, role role, vmID int, createdAt time.Tim Timestamp: time.Now(), Metadata: fmt.Sprintf( "Virtual machine %v which created at %v, is deleted", - vmID, createdAt, + vmID, createdAt.Format("January 2, 2006"), ), } @@ -334,7 +334,7 @@ func (a *App) logInvoiceCreate(userID, currency string, invoiceID int, invoiceTo Timestamp: time.Now(), Metadata: fmt.Sprintf( "Invoice %v with value: %v %v is created at %v", - invoiceID, invoiceTotal, currency, createdAt, + invoiceID, invoiceTotal, currency, createdAt.Format("January 2, 2006"), ), } diff --git a/server/app/notification_handler.go b/server/app/notification_handler.go index d121a758..c3e7dfc9 100644 --- a/server/app/notification_handler.go +++ b/server/app/notification_handler.go @@ -62,8 +62,6 @@ func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response // @Failure 500 {object} Response // @Router /notification/{id} [put] func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Response) { - userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) - id, err := strconv.Atoi(mux.Vars(req)["id"]) if err != nil { log.Error().Err(err).Send()