Compare commits

..

30 Commits

Author SHA1 Message Date
grandwizard28
948eab368c feat(apiserver): instrument remaining service-account routes + fix user-role attach
- Add audit instrumentation for the 5 SA routes missed in the original pass:
  POST /api/v1/service_accounts (Create), PUT /{id} (Update), PUT /me, DELETE
  /{id}, PUT /{id}/keys/{fid} (factor-api-key update).
- Add AttachAuditDef for the SA role attach/detach routes:
  POST /api/v1/service_accounts/{id}/roles (role id from body)
  DELETE /api/v1/service_accounts/{id}/roles/{rid} (role id from path)
- Fix user-role attach: PostableRole has 'name' not 'id'; BodyJSONPath now
  reads name. The role identifier in the role-attach body is the role name.
- Switch session Create to ResourceMetaResourcesSession (plural) to match
  the Create-uses-plural convention applied elsewhere.
2026-05-11 16:35:41 +05:30
grandwizard28
d84327e08e feat(audit): fan out attach events from request-body entity lists
Adds AttachManyAuditDef + BodyJSONArray extractor. The middleware now emits
one attach event per id returned by AttachedResourceIDs, in addition to the
primary create event. Applied to routes whose request body carries a list
of references to other entities:

- POST /api/v2/rules — preferredChannels → notification-channel.attached to rule
- POST /api/v1/downtime_schedules — alertIds → rule.attached to planned-maintenance
- POST /api/v1/route_policies — channels → notification-channel.attached to route-policy

Also fans out simple create+attach pairs for nested resources where the
child has its own identity:

- POST /api/v2/gateway/ingestion_keys/{keyId}/limits → ingestion-limit.created
  + ingestion-limit.attached to ingestion-key
- POST /api/v1/span_mapper_groups/{groupId}/span_mappers → span-mapper.created
  + span-mapper.attached to span-mapper-group

All create events now use ResourceMetaResources (plural) and pull the new
id from data.id via ResponseJSONPath.
2026-05-11 16:14:20 +05:30
grandwizard28
ac280d9b9c feat(audit): add ResponseJSONPath extractor and pre-buffer request body
ResourceIDExtractor now takes an ExtractorContext bundling the request, the
pre-buffered request body, and the captured response body. Adds:

- ResponseJSONPath(path) reads the captured response body via gjson, for
  Create routes where the new resource id is only known after the handler
  writes the response.
- BodyJSONPath now reads from the pre-buffered request bytes rather than
  re-reading req.Body. This fixes the latent bug where the handler would
  have already consumed the body before the extractor ran.

The middleware pre-buffers the request body when the matched route declares
any AuditDefs, restoring req.Body via NopCloser so the handler still sees
the full payload.

SA factor-api-key POST now pulls the new key's id from the response body
(data.id) for both the created and attached events.
2026-05-11 16:05:34 +05:30
grandwizard28
cfeae11ade feat(audit): allow multiple AuditDefs per route, variadic WithAuditDef
Single endpoints can produce multiple semantically distinct audit events.
The first concrete case is POST /api/v1/service_accounts/{id}/keys, which
both creates a factor-api-key and attaches it to a service account.

- WithAuditDef becomes variadic; handler stores []AuditDef
- Handler interface returns AuditDefs() []AuditDef
- Middleware loops and emits one audit event per def
- SA factor-api-key POST now emits factor-api-key.created (collection-level)
  plus factor-api-key.attached to the parent service-account
2026-05-11 15:58:53 +05:30
grandwizard28
c0af4e1135 feat(coretypes): rename KindMetric to KindMetricField
Mirror the logs-field / traces-field naming. The /api/v2/metrics/{metric_name}/metadata
audit subject is the metric field (the metric name registered for the org), not a
metric series instance.
2026-05-11 15:32:04 +05:30
grandwizard28
cd82a63369 feat(apiserver): instrument zeus profile and host routes with AuditDef 2026-05-11 14:18:28 +05:30
grandwizard28
ef293e1505 feat(apiserver): instrument span-mapper-group and span-mapper routes with AuditDef 2026-05-11 14:17:29 +05:30
grandwizard28
2eab9474c0 feat(apiserver): instrument role routes with AuditDef 2026-05-11 14:16:44 +05:30
grandwizard28
132bf8478a feat(apiserver): instrument promote-and-index paths route with AuditDef 2026-05-11 14:16:05 +05:30
grandwizard28
68ab3917ee feat(apiserver): instrument user-preference and org-preference routes with AuditDef 2026-05-11 14:15:44 +05:30
grandwizard28
52516eeec5 feat(apiserver): instrument organization update route with AuditDef 2026-05-11 14:15:02 +05:30
grandwizard28
93eb8c7419 feat(apiserver): instrument metric-metadata route with AuditDef 2026-05-11 14:14:18 +05:30
grandwizard28
47e5507490 feat(apiserver): instrument llm-pricing-rule routes with AuditDef 2026-05-11 14:13:55 +05:30
grandwizard28
f9e296de9f feat(apiserver): instrument ingestion-key and ingestion-limit routes with AuditDef 2026-05-11 14:13:15 +05:30
grandwizard28
4ff9323dc8 feat(apiserver): instrument public-dashboard routes with AuditDef 2026-05-11 14:12:28 +05:30
grandwizard28
f8570e9713 feat(apiserver): instrument cloud-integration routes with AuditDef 2026-05-11 14:11:48 +05:30
grandwizard28
ecc748c811 feat(apiserver): instrument auth-domain routes with AuditDef 2026-05-11 14:11:08 +05:30
grandwizard28
a5db2c1fca feat(apiserver): instrument notification-channel and route-policy routes with AuditDef 2026-05-11 14:10:36 +05:30
grandwizard28
8653dd3c5c feat(coretypes): register kinds for llm-pricing-rule, span mappers, zeus, metric 2026-05-11 14:09:42 +05:30
grandwizard28
aa3f45a2c7 refactor(audit): make AuditDef a sealed interface; use coretypes.Resource
AuditDef becomes an interface with two implementations: BasicAuditDef
for single-resource routes and AttachAuditDef for routes that attach
one resource to another. Resource ids are pulled via a ResourceIDExtractor
function — PathParam for mux vars, BodyJSONPath for body fields via gjson.

ResourceAttributes carries a coretypes.Resource (not just a Kind) so the
middleware can emit signoz.audit.resource.object via Resource.Object(orgID,
id) alongside resource.kind / resource.id. Attach events also emit
signoz.audit.target.{kind,id,object} for the target side of the attachment.

Routes:
- session, user, factor-password, factor-api-key, rule, planned-maintenance
  use BasicAuditDef with the matching ResourceMetaResourceX (or ResourceUser).
- POST /api/v2/users/{id}/roles and DELETE /api/v2/users/{id}/roles/{roleId}
  use AttachAuditDef — event name role.attached. POST extracts role id from
  body via gjson; DELETE pulls roleId from the path.
- Reset-password-token route emits factor-password.updated (was the bad
  reset-password-token kind).

Deletes KindUserRole and KindResetPasswordToken from the kind registry.

Affected unit tests deleted; the integration tests under
tests/integration/tests/auditor still reference the old event names and
will be rewritten in a follow-up.
2026-05-11 13:59:48 +05:30
grandwizard28
cb7b40cd7c refactor(audit): rename Action field to Verb on AuditDef and AuditAttributes
Drop the Action alias; the field has always held a coretypes.Verb value.
Rename the OTel attribute key signoz.audit.action to signoz.audit.verb
to match. ActionCategory is a distinct enum and stays as-is.
2026-05-11 13:06:36 +05:30
grandwizard28
9ebcf765d0 test(auditor): move shared helpers from suite conftest to fixtures/auditor.py
Mirrors the existing fixtures/audit.py / fixtures/auth.py / fixtures/signoz.py
layout — domain-named module under fixtures/ registered via pytest_plugins,
suite-local conftest.py kept thin (only the signoz fixture override that
configures the auditor container).
2026-05-11 12:31:25 +05:30
grandwizard28
e5ebd86ad6 test(auditor): bind-mount audit dir and atomic batch write
Mirror the sqlite/clickhouse pattern: the audit log file lives in a tmpfs
directory bind-mounted into the SigNoz container at the same absolute path,
so tests read it via a plain open() instead of docker exec. The directory
path is cached via reuse.wrap so the long-lived auditor container under
--reuse keeps using the matching mount across pytest runs.

The file provider's export now writes payload + newline in a single Write
call so a concurrent host reader cannot observe a torn JSON object.
2026-05-11 12:31:19 +05:30
grandwizard28
72e1d1aace test(auditor): integration tests for file provider event emission 2026-05-11 12:27:42 +05:30
grandwizard28
b396b16a1e feat(apiserver): instrument rule and planned-maintenance routes with AuditDef 2026-05-11 12:25:08 +05:30
grandwizard28
7a90ca5e9e feat(apiserver): instrument service account API key routes with AuditDef 2026-05-11 12:24:07 +05:30
grandwizard28
46a7b1b1ff feat(apiserver): instrument user v2 P0 routes with AuditDef 2026-05-11 12:21:51 +05:30
grandwizard28
23ee24732d feat(apiserver): instrument session P0 routes with AuditDef 2026-05-11 12:20:09 +05:30
grandwizard28
ed1ab29307 feat(coretypes): register user-role and reset-password-token kinds 2026-05-11 12:19:28 +05:30
Pandey
a2b4a685ad feat(auditor): add file provider (#11252)
* feat(auditor): add file provider for audit logs

* feat(auditor): write payload + newline in a single syscall

Combine the JSON payload and trailing newline into one Write call so a
concurrent reader can never observe a torn JSON object — they see either
the full line or nothing.

* style(auditor): drop wrapping at file-provider export and obvious doc comments
2026-05-11 06:23:42 +00:00
47 changed files with 2407 additions and 1663 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/auditor/fileauditor"
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
@@ -155,6 +156,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
if err := factories.Add(otlphttpauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
if err := factories.Add(fileauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
return factories
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {

View File

@@ -0,0 +1,33 @@
package fileauditor
import (
"context"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/audittypes"
)
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
payload, err := provider.marshaler.MarshalLogs(logs)
if err != nil {
return err
}
// Combine the payload and trailing newline into one Write call so the line
// is emitted in a single syscall — concurrent readers see either the full
// line or nothing, never a torn JSON object.
payload = append(payload, '\n')
provider.mu.Lock()
defer provider.mu.Unlock()
if _, err := provider.file.Write(payload); err != nil {
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
return err
}
return provider.file.Sync()
}

View File

@@ -0,0 +1,100 @@
package fileauditor
import (
"context"
"os"
"sync"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/version"
"go.opentelemetry.io/collector/pdata/plog"
)
var _ auditor.Auditor = (*provider)(nil)
type provider struct {
settings factory.ScopedProviderSettings
config auditor.Config
licensing licensing.Licensing
build version.Build
server *auditorserver.Server
marshaler plog.JSONMarshaler
file *os.File
mu sync.Mutex
}
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("file"), func(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
return newProvider(ctx, providerSettings, config, licensing, build)
})
}
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config auditor.Config, licensing licensing.Licensing, build version.Build) (auditor.Auditor, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/auditor/fileauditor")
file, err := os.OpenFile(config.File.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, auditor.ErrCodeAuditExportFailed, "failed to open audit file %q", config.File.Path)
}
provider := &provider{
settings: settings,
config: config,
licensing: licensing,
build: build,
marshaler: plog.JSONMarshaler{},
file: file,
}
server, err := auditorserver.New(settings,
auditorserver.Config{
BufferSize: config.BufferSize,
BatchSize: config.BatchSize,
FlushInterval: config.FlushInterval,
},
provider.export,
)
if err != nil {
_ = file.Close()
return nil, err
}
provider.server = server
return provider, nil
}
func (provider *provider) Start(ctx context.Context) error {
return provider.server.Start(ctx)
}
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
return
}
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
return
}
provider.server.Add(ctx, event)
}
func (provider *provider) Healthy() <-chan struct{} {
return provider.server.Healthy()
}
func (provider *provider) Stop(ctx context.Context) error {
serverErr := provider.server.Stop(ctx)
fileErr := provider.file.Close()
if serverErr != nil {
return serverErr
}
return fileErr
}

View File

@@ -6,6 +6,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
@@ -44,54 +46,80 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/channels", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
ID: "CreateChannel",
Tags: []string{"channels"},
Summary: "Create notification channel",
Description: "This endpoint creates a notification channel",
Request: new(alertmanagertypes.PostableChannel),
RequestContentType: "application/json",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/channels", handler.New(
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateChannel),
handler.OpenAPIDef{
ID: "CreateChannel",
Tags: []string{"channels"},
Summary: "Create notification channel",
Description: "This endpoint creates a notification channel",
Request: new(alertmanagertypes.PostableChannel),
RequestContentType: "application/json",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesNotificationChannel,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
ID: "UpdateChannelByID",
Tags: []string{"channels"},
Summary: "Update notification channel",
Description: "This endpoint updates a notification channel by ID",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/channels/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID),
handler.OpenAPIDef{
ID: "UpdateChannelByID",
Tags: []string{"channels"},
Summary: "Update notification channel",
Description: "This endpoint updates a notification channel by ID",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceNotificationChannel,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
ID: "DeleteChannelByID",
Tags: []string{"channels"},
Summary: "Delete notification channel",
Description: "This endpoint deletes a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/channels/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID),
handler.OpenAPIDef{
ID: "DeleteChannelByID",
Tags: []string{"channels"},
Summary: "Delete notification channel",
Description: "This endpoint deletes a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceNotificationChannel,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -163,54 +191,91 @@ func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
ID: "CreateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Create route policy",
Description: "This endpoint creates a route policy",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/route_policies", handler.New(
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy),
handler.OpenAPIDef{
ID: "CreateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Create route policy",
Description: "This endpoint creates a route policy",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(
handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesRoutePolicy,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
},
handler.AttachManyAuditDef{
AttachedResource: coretypes.ResourceMetaResourceNotificationChannel,
AttachedResourceIDs: handler.BodyJSONArray("channels"),
TargetResource: coretypes.ResourceMetaResourceRoutePolicy,
TargetResourceID: handler.ResponseJSONPath("data.id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryConfigurationChange,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
ID: "UpdateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Update route policy",
Description: "This endpoint updates a route policy by ID",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy),
handler.OpenAPIDef{
ID: "UpdateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Update route policy",
Description: "This endpoint updates a route policy by ID",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceRoutePolicy,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
ID: "DeleteRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Delete route policy",
Description: "This endpoint deletes a route policy by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID),
handler.OpenAPIDef{
ID: "DeleteRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Delete route policy",
Description: "This endpoint deletes a route policy by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceRoutePolicy,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,7 +5,9 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
@@ -27,20 +29,28 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
ID: "CreateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Create auth domain",
Description: "This endpoint creates an auth domain",
Request: new(authtypes.PostableAuthDomain),
RequestContentType: "application/json",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/domains", handler.New(
provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create),
handler.OpenAPIDef{
ID: "CreateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Create auth domain",
Description: "This endpoint creates an auth domain",
Request: new(authtypes.PostableAuthDomain),
RequestContentType: "application/json",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesAuthDomain,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -61,37 +71,55 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
ID: "UpdateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Update auth domain",
Description: "This endpoint updates an auth domain",
Request: new(authtypes.UpdatableAuthDomain),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/domains/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update),
handler.OpenAPIDef{
ID: "UpdateAuthDomain",
Tags: []string{"authdomains"},
Summary: "Update auth domain",
Description: "This endpoint updates an auth domain",
Request: new(authtypes.UpdatableAuthDomain),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceAuthDomain,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
ID: "DeleteAuthDomain",
Tags: []string{"authdomains"},
Summary: "Delete auth domain",
Description: "This endpoint deletes an auth domain",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/domains/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete),
handler.OpenAPIDef{
ID: "DeleteAuthDomain",
Tags: []string{"authdomains"},
Summary: "Delete auth domain",
Description: "This endpoint deletes an auth domain",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceAuthDomain,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,7 +5,9 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
@@ -46,6 +48,11 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesCloudIntegration,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -106,6 +113,12 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -126,6 +139,12 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceCloudIntegration,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -188,6 +207,12 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceCloudIntegrationService,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("service_id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
@@ -14,20 +15,29 @@ import (
)
func (provider *provider) addDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic), handler.OpenAPIDef{
ID: "CreatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Create public dashboard",
Description: "This endpoint creates public sharing config and enables public sharing of the dashboard",
Request: new(dashboardtypes.PostablePublicDashboard),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(
provider.authzMiddleware.AdminAccess(provider.dashboardHandler.CreatePublic),
handler.OpenAPIDef{
ID: "CreatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Create public dashboard",
Description: "This endpoint creates public sharing config and enables public sharing of the dashboard",
Request: new(dashboardtypes.PostablePublicDashboard),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesPublicDashboard,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -48,37 +58,55 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.UpdatePublic), handler.OpenAPIDef{
ID: "UpdatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Update public dashboard",
Description: "This endpoint updates the public sharing config for a dashboard",
Request: new(dashboardtypes.UpdatablePublicDashboard),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(
provider.authzMiddleware.AdminAccess(provider.dashboardHandler.UpdatePublic),
handler.OpenAPIDef{
ID: "UpdatePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Update public dashboard",
Description: "This endpoint updates the public sharing config for a dashboard",
Request: new(dashboardtypes.UpdatablePublicDashboard),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcePublicDashboard,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(provider.authzMiddleware.AdminAccess(provider.dashboardHandler.DeletePublic), handler.OpenAPIDef{
ID: "DeletePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Delete public dashboard",
Description: "This endpoint deletes the public sharing config and disables the public sharing of a dashboard",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/dashboards/{id}/public", handler.New(
provider.authzMiddleware.AdminAccess(provider.dashboardHandler.DeletePublic),
handler.OpenAPIDef{
ID: "DeletePublicDashboard",
Tags: []string{"dashboard"},
Summary: "Delete public dashboard",
Description: "This endpoint deletes the public sharing config and disables the public sharing of a dashboard",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcePublicDashboard,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
"github.com/gorilla/mux"
)
@@ -46,105 +48,169 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKey), handler.OpenAPIDef{
ID: "CreateIngestionKey",
Tags: []string{"gateway"},
Summary: "Create ingestion key for workspace",
Description: "This endpoint creates an ingestion key for the workspace",
Request: new(gatewaytypes.PostableIngestionKey),
RequestContentType: "application/json",
Response: new(gatewaytypes.GettableCreatedIngestionKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(
provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKey),
handler.OpenAPIDef{
ID: "CreateIngestionKey",
Tags: []string{"gateway"},
Summary: "Create ingestion key for workspace",
Description: "This endpoint creates an ingestion key for the workspace",
Request: new(gatewaytypes.PostableIngestionKey),
RequestContentType: "application/json",
Response: new(gatewaytypes.GettableCreatedIngestionKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesIngestionKey,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKey), handler.OpenAPIDef{
ID: "UpdateIngestionKey",
Tags: []string{"gateway"},
Summary: "Update ingestion key for workspace",
Description: "This endpoint updates an ingestion key for the workspace",
Request: new(gatewaytypes.PostableIngestionKey),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(
provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKey),
handler.OpenAPIDef{
ID: "UpdateIngestionKey",
Tags: []string{"gateway"},
Summary: "Update ingestion key for workspace",
Description: "This endpoint updates an ingestion key for the workspace",
Request: new(gatewaytypes.PostableIngestionKey),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceIngestionKey,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("keyId"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKey), handler.OpenAPIDef{
ID: "DeleteIngestionKey",
Tags: []string{"gateway"},
Summary: "Delete ingestion key for workspace",
Description: "This endpoint deletes an ingestion key for the workspace",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(
provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKey),
handler.OpenAPIDef{
ID: "DeleteIngestionKey",
Tags: []string{"gateway"},
Summary: "Delete ingestion key for workspace",
Description: "This endpoint deletes an ingestion key for the workspace",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceIngestionKey,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("keyId"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
ID: "CreateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Create limit for the ingestion key",
Description: "This endpoint creates an ingestion key limit",
Request: new(gatewaytypes.PostableIngestionKeyLimit),
RequestContentType: "application/json",
Response: new(gatewaytypes.GettableCreatedIngestionKeyLimit),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(
provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit),
handler.OpenAPIDef{
ID: "CreateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Create limit for the ingestion key",
Description: "This endpoint creates an ingestion key limit",
Request: new(gatewaytypes.PostableIngestionKeyLimit),
RequestContentType: "application/json",
Response: new(gatewaytypes.GettableCreatedIngestionKeyLimit),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(
handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesIngestionLimit,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
},
handler.AttachAuditDef{
AttachedResource: coretypes.ResourceMetaResourceIngestionLimit,
AttachedResourceID: handler.ResponseJSONPath("data.id"),
TargetResource: coretypes.ResourceMetaResourceIngestionKey,
TargetResourceID: handler.PathParam("keyId"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryConfigurationChange,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
ID: "UpdateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Update limit for the ingestion key",
Description: "This endpoint updates an ingestion key limit",
Request: new(gatewaytypes.UpdatableIngestionKeyLimit),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(
provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit),
handler.OpenAPIDef{
ID: "UpdateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Update limit for the ingestion key",
Description: "This endpoint updates an ingestion key limit",
Request: new(gatewaytypes.UpdatableIngestionKeyLimit),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceIngestionLimit,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("limitId"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
ID: "DeleteIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Delete limit for the ingestion key",
Description: "This endpoint deletes an ingestion key limit",
Request: nil,
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(
provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit),
handler.OpenAPIDef{
ID: "DeleteIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Delete limit for the ingestion key",
Description: "This endpoint deletes an ingestion key limit",
Request: nil,
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceIngestionLimit,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("limitId"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
"github.com/gorilla/mux"
)
@@ -45,6 +47,11 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesLLMPricingRule,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -85,6 +92,12 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceLLMPricingRule,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
"github.com/gorilla/mux"
)
@@ -122,7 +124,14 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceMetricField,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("metric_name"),
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
@@ -26,20 +28,28 @@ func (provider *provider) addOrgRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/orgs/me", handler.New(provider.authzMiddleware.AdminAccess(provider.orgHandler.Update), handler.OpenAPIDef{
ID: "UpdateMyOrganization",
Tags: []string{"orgs"},
Summary: "Update my organization",
Description: "This endpoint updates the organization I belong to",
Request: new(types.Organization),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/orgs/me", handler.New(
provider.authzMiddleware.AdminAccess(provider.orgHandler.Update),
handler.OpenAPIDef{
ID: "UpdateMyOrganization",
Tags: []string{"orgs"},
Summary: "Update my organization",
Description: "This endpoint updates the organization I belong to",
Request: new(types.Organization),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusConflict, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceOrganization,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/preferencetypes"
"github.com/gorilla/mux"
)
@@ -44,20 +46,29 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(provider.authzMiddleware.ViewAccess(provider.preferenceHandler.UpdateByUser), handler.OpenAPIDef{
ID: "UpdateUserPreference",
Tags: []string{"preferences"},
Summary: "Update user preference",
Description: "This endpoint updates the user preference by name",
Request: new(preferencetypes.UpdatablePreference),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/user/preferences/{name}", handler.New(
provider.authzMiddleware.ViewAccess(provider.preferenceHandler.UpdateByUser),
handler.OpenAPIDef{
ID: "UpdateUserPreference",
Tags: []string{"preferences"},
Summary: "Update user preference",
Description: "This endpoint updates the user preference by name",
Request: new(preferencetypes.UpdatablePreference),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceUserPreference,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("name"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -95,20 +106,29 @@ func (provider *provider) addPreferenceRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg), handler.OpenAPIDef{
ID: "UpdateOrgPreference",
Tags: []string{"preferences"},
Summary: "Update org preference",
Description: "This endpoint updates the org preference by name",
Request: new(preferencetypes.UpdatablePreference),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/org/preferences/{name}", handler.New(
provider.authzMiddleware.AdminAccess(provider.preferenceHandler.UpdateByOrg),
handler.OpenAPIDef{
ID: "UpdateOrgPreference",
Tags: []string{"preferences"},
Summary: "Update org preference",
Description: "This endpoint updates the org preference by name",
Request: new(preferencetypes.UpdatablePreference),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceOrgPreference,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("name"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -5,24 +5,34 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/promotetypes"
"github.com/gorilla/mux"
)
func (provider *provider) addPromoteRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(provider.authzMiddleware.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths), handler.OpenAPIDef{
ID: "HandlePromoteAndIndexPaths",
Tags: []string{"logs"},
Summary: "Promote and index paths",
Description: "This endpoints promotes and indexes paths",
Request: new([]*promotetypes.PromotePath),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/logs/promote_paths", handler.New(
provider.authzMiddleware.EditAccess(provider.promoteHandler.HandlePromoteAndIndexPaths),
handler.OpenAPIDef{
ID: "HandlePromoteAndIndexPaths",
Tags: []string{"logs"},
Summary: "Promote and index paths",
Description: "This endpoints promotes and indexes paths",
Request: new([]*promotetypes.PromotePath),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesLogsField,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}

View File

@@ -50,7 +50,7 @@ func (handler *healthOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext
)
}
func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef {
func (handler *healthOpenAPIHandler) AuditDefs() []pkghandler.AuditDef {
// Health endpoints are not audited since they don't represent user actions and are called frequently by monitoring systems, which would create noise in the audit logs.
return nil
}

View File

@@ -5,26 +5,35 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
func (provider *provider) addRoleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Create), handler.OpenAPIDef{
ID: "CreateRole",
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: new(authtypes.PostableRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/roles", handler.New(
provider.authzMiddleware.AdminAccess(provider.authzHandler.Create),
handler.OpenAPIDef{
ID: "CreateRole",
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: new(authtypes.PostableRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesRole,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -79,54 +88,81 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch), handler.OpenAPIDef{
ID: "PatchRole",
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: new(authtypes.PatchableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.authzHandler.Patch),
handler.OpenAPIDef{
ID: "PatchRole",
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: new(authtypes.PatchableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects), handler.OpenAPIDef{
ID: "PatchObjects",
Tags: []string{"role"},
Summary: "Patch objects for a role by relation",
Description: "Patches the objects connected to the specified role via a given relation type",
Request: new(coretypes.PatchableObjects),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(
provider.authzMiddleware.AdminAccess(provider.authzHandler.PatchObjects),
handler.OpenAPIDef{
ID: "PatchObjects",
Tags: []string{"role"},
Summary: "Patch objects for a role by relation",
Description: "Patches the objects connected to the specified role via a given relation type",
Request: new(coretypes.PatchableObjects),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete), handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",
Description: "This endpoint deletes a role",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.authzHandler.Delete),
handler.OpenAPIDef{
ID: "DeleteRole",
Tags: []string{"role"},
Summary: "Delete role",
Description: "This endpoint deletes a role",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/gorilla/mux"
)
@@ -37,64 +39,110 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateRule), handler.OpenAPIDef{
ID: "CreateRule",
Tags: []string{"rules"},
Summary: "Create alert rule",
Description: "This endpoint creates a new alert rule",
Request: new(ruletypes.PostableRule),
RequestContentType: "application/json",
RequestExamples: postableRuleExamples(),
Response: new(ruletypes.Rule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/rules", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateRule),
handler.OpenAPIDef{
ID: "CreateRule",
Tags: []string{"rules"},
Summary: "Create alert rule",
Description: "This endpoint creates a new alert rule",
Request: new(ruletypes.PostableRule),
RequestContentType: "application/json",
RequestExamples: postableRuleExamples(),
Response: new(ruletypes.Rule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(
handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesRule,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
},
handler.AttachManyAuditDef{
AttachedResource: coretypes.ResourceMetaResourceNotificationChannel,
AttachedResourceIDs: handler.BodyJSONArray("preferredChannels"),
TargetResource: coretypes.ResourceMetaResourceRule,
TargetResourceID: handler.ResponseJSONPath("data.id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryConfigurationChange,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateRuleByID), handler.OpenAPIDef{
ID: "UpdateRuleByID",
Tags: []string{"rules"},
Summary: "Update alert rule",
Description: "This endpoint updates an alert rule by ID",
Request: new(ruletypes.PostableRule),
RequestContentType: "application/json",
RequestExamples: postableRuleExamples(),
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/rules/{id}", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateRuleByID),
handler.OpenAPIDef{
ID: "UpdateRuleByID",
Tags: []string{"rules"},
Summary: "Update alert rule",
Description: "This endpoint updates an alert rule by ID",
Request: new(ruletypes.PostableRule),
RequestContentType: "application/json",
RequestExamples: postableRuleExamples(),
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteRuleByID), handler.OpenAPIDef{
ID: "DeleteRuleByID",
Tags: []string{"rules"},
Summary: "Delete alert rule",
Description: "This endpoint deletes an alert rule by ID",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v2/rules/{id}", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteRuleByID),
handler.OpenAPIDef{
ID: "DeleteRuleByID",
Tags: []string{"rules"},
Summary: "Delete alert rule",
Description: "This endpoint deletes an alert rule by ID",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.PatchRuleByID), handler.OpenAPIDef{
ID: "PatchRuleByID",
Tags: []string{"rules"},
Summary: "Patch alert rule",
Description: "This endpoint applies a partial update to an alert rule by ID",
Request: new(ruletypes.PostableRule),
RequestContentType: "application/json",
RequestExamples: postableRuleExamples(),
Response: new(ruletypes.Rule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v2/rules/{id}", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.PatchRuleByID),
handler.OpenAPIDef{
ID: "PatchRuleByID",
Tags: []string{"rules"},
Summary: "Patch alert rule",
Description: "This endpoint applies a partial update to an alert rule by ID",
Request: new(ruletypes.PostableRule),
RequestContentType: "application/json",
RequestExamples: postableRuleExamples(),
Response: new(ruletypes.Rule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -143,45 +191,82 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateDowntimeSchedule), handler.OpenAPIDef{
ID: "CreateDowntimeSchedule",
Tags: []string{"downtimeschedules"},
Summary: "Create downtime schedule",
Description: "This endpoint creates a new planned maintenance / downtime schedule",
Request: new(ruletypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
Response: new(ruletypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateDowntimeSchedule),
handler.OpenAPIDef{
ID: "CreateDowntimeSchedule",
Tags: []string{"downtimeschedules"},
Summary: "Create downtime schedule",
Description: "This endpoint creates a new planned maintenance / downtime schedule",
Request: new(ruletypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
Response: new(ruletypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(
handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesPlannedMaintenance,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
},
handler.AttachManyAuditDef{
AttachedResource: coretypes.ResourceMetaResourceRule,
AttachedResourceIDs: handler.BodyJSONArray("alertIds"),
TargetResource: coretypes.ResourceMetaResourcePlannedMaintenance,
TargetResourceID: handler.ResponseJSONPath("data.id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryConfigurationChange,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
ID: "UpdateDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Update downtime schedule",
Description: "This endpoint updates a downtime schedule by ID",
Request: new(ruletypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID),
handler.OpenAPIDef{
ID: "UpdateDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Update downtime schedule",
Description: "This endpoint updates a downtime schedule by ID",
Request: new(ruletypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
ID: "DeleteDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Delete downtime schedule",
Description: "This endpoint deletes a downtime schedule by ID",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID),
handler.OpenAPIDef{
ID: "DeleteDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Delete downtime schedule",
Description: "This endpoint deletes a downtime schedule by ID",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
@@ -17,22 +18,31 @@ import (
)
func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "CreateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Create service account",
Description: "This endpoint creates a service account",
Request: new(serviceaccounttypes.PostableServiceAccount),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts", handler.New(
provider.authzMiddleware.Check(provider.serviceAccountHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceMetaResourcesServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}),
handler.OpenAPIDef{
ID: "CreateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Create service account",
Description: "This endpoint creates a service account",
Request: new(serviceaccounttypes.PostableServiceAccount),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcesServiceAccount.Scope(coretypes.VerbCreate)}),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesServiceAccount,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.ResponseJSONPath("data.id"),
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -110,125 +120,192 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.SetRole, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(
provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.SetRole, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromBody, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}),
handler.OpenAPIDef{
ID: "CreateServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Create service account role",
Description: "This endpoint assigns a role to a service account",
Request: new(serviceaccounttypes.PostableServiceAccountRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
},
handler.WithAuditDef(handler.AttachAuditDef{
AttachedResource: coretypes.ResourceRole,
AttachedResourceID: handler.BodyJSONPath("id"),
TargetResource: coretypes.ResourceServiceAccount,
TargetResourceID: handler.PathParam("id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(
provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.DeleteRole, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}),
handler.OpenAPIDef{
ID: "DeleteServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Delete service account role",
Description: "This endpoint revokes a role from service account",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
},
handler.WithAuditDef(handler.AttachAuditDef{
AttachedResource: coretypes.ResourceRole,
AttachedResourceID: handler.PathParam("rid"),
TargetResource: coretypes.ResourceServiceAccount,
TargetResourceID: handler.PathParam("id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/me", handler.New(
provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.UpdateMe),
handler.OpenAPIDef{
ID: "UpdateMyServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates my service account",
Description: "This endpoint gets my service account",
Request: new(serviceaccounttypes.UpdatableServiceAccount),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: nil,
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
provider.authzMiddleware.Check(provider.serviceAccountHandler.Update, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromBody, Roles: []string{
}),
handler.OpenAPIDef{
ID: "UpdateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account",
Description: "This endpoint updates an existing service account",
Request: new(serviceaccounttypes.UpdatableServiceAccount),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
provider.authzMiddleware.Check(provider.serviceAccountHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
ID: "CreateServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Create service account role",
Description: "This endpoint assigns a role to a service account",
Request: new(serviceaccounttypes.PostableServiceAccountRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
})).Methods(http.MethodPost).GetError(); err != nil {
}),
handler.OpenAPIDef{
ID: "DeleteServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Deletes a service account",
Description: "This endpoint deletes an existing service account",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDelete)}),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.DeleteRole, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(
provider.authzMiddleware.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleAttachSelectorFromPath, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
}), handler.OpenAPIDef{
ID: "DeleteServiceAccountRole",
Tags: []string{"serviceaccount"},
Summary: "Delete service account role",
Description: "This endpoint revokes a role from service account",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach), coretypes.ResourceRole.Scope(coretypes.VerbAttach)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/me", handler.New(provider.authzMiddleware.OpenAccess(provider.serviceAccountHandler.UpdateMe), handler.OpenAPIDef{
ID: "UpdateMyServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates my service account",
Description: "This endpoint gets my service account",
Request: new(serviceaccounttypes.UpdatableServiceAccount),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: nil,
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Update, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "UpdateServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account",
Description: "This endpoint updates an existing service account",
Request: new(serviceaccounttypes.UpdatableServiceAccount),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "DeleteServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Deletes a service account",
Description: "This endpoint deletes an existing service account",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbDelete)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.CreateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "CreateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Create a service account key",
Description: "This endpoint creates a service account key",
Request: new(serviceaccounttypes.PostableFactorAPIKey),
RequestContentType: "",
Response: new(serviceaccounttypes.GettableFactorAPIKeyWithKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
}),
handler.OpenAPIDef{
ID: "CreateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Create a service account key",
Description: "This endpoint creates a service account key",
Request: new(serviceaccounttypes.PostableFactorAPIKey),
RequestContentType: "",
Response: new(serviceaccounttypes.GettableFactorAPIKeyWithKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
},
handler.WithAuditDef(
handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesFactorAPIKey,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.ResponseJSONPath("data.id"),
},
handler.AttachAuditDef{
AttachedResource: coretypes.ResourceMetaResourceFactorAPIKey,
AttachedResourceID: handler.ResponseJSONPath("data.id"),
TargetResource: coretypes.ResourceServiceAccount,
TargetResourceID: handler.PathParam("id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryAccessControl,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -251,41 +328,59 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "UpdateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account key",
Description: "This endpoint updates an existing service account key",
Request: new(serviceaccounttypes.UpdatableFactorAPIKey),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(
provider.authzMiddleware.Check(provider.serviceAccountHandler.UpdateFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}),
handler.OpenAPIDef{
ID: "UpdateServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Updates a service account key",
Description: "This endpoint updates an existing service account key",
Request: new(serviceaccounttypes.UpdatableFactorAPIKey),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("fid"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "RevokeServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Revoke a service account key",
Description: "This endpoint revokes an existing service account key",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(
provider.authzMiddleware.Check(provider.serviceAccountHandler.RevokeFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}),
handler.OpenAPIDef{
ID: "RevokeServiceAccountKey",
Tags: []string{"serviceaccount"},
Summary: "Revoke a service account key",
Description: "This endpoint revokes an existing service account key",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbUpdate)}),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -4,25 +4,35 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
func (provider *provider) addSessionRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/sessions/email_password", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword), handler.OpenAPIDef{
ID: "CreateSessionByEmailPassword",
Tags: []string{"sessions"},
Summary: "Create session by email and password",
Description: "This endpoint creates a session for a user using email and password.",
Request: new(authtypes.PostableEmailPasswordSession),
RequestContentType: "application/json",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/sessions/email_password", handler.New(
provider.authzMiddleware.OpenAccess(provider.sessionHandler.CreateSessionByEmailPassword),
handler.OpenAPIDef{
ID: "CreateSessionByEmailPassword",
Tags: []string{"sessions"},
Summary: "Create session by email and password",
Description: "This endpoint creates a session for a user using email and password.",
Request: new(authtypes.PostableEmailPasswordSession),
RequestContentType: "application/json",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesSession,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -43,37 +53,53 @@ func (provider *provider) addSessionRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/sessions/rotate", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.RotateSession), handler.OpenAPIDef{
ID: "RotateSession",
Tags: []string{"sessions"},
Summary: "Rotate session",
Description: "This endpoint rotates the session",
Request: new(authtypes.PostableRotateToken),
RequestContentType: "application/json",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/sessions/rotate", handler.New(
provider.authzMiddleware.OpenAccess(provider.sessionHandler.RotateSession),
handler.OpenAPIDef{
ID: "RotateSession",
Tags: []string{"sessions"},
Summary: "Rotate session",
Description: "This endpoint rotates the session",
Request: new(authtypes.PostableRotateToken),
RequestContentType: "application/json",
Response: new(authtypes.GettableToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceSession,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/sessions", handler.New(provider.authzMiddleware.OpenAccess(provider.sessionHandler.DeleteSession), handler.OpenAPIDef{
ID: "DeleteSession",
Tags: []string{"sessions"},
Summary: "Delete session",
Description: "This endpoint deletes the session",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v2/sessions", handler.New(
provider.authzMiddleware.OpenAccess(provider.sessionHandler.DeleteSession),
handler.OpenAPIDef{
ID: "DeleteSession",
Tags: []string{"sessions"},
Summary: "Delete session",
Description: "This endpoint deletes the session",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceSession,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/gorilla/mux"
)
@@ -47,6 +49,12 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesSpanMapperGroup,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -65,6 +73,12 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceSpanMapperGroup,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("groupId"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -85,6 +99,12 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceSpanMapperGroup,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("groupId"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -125,6 +145,22 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(
handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourcesSpanMapper,
Verb: coretypes.VerbCreate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.ResponseJSONPath("data.id"),
},
handler.AttachAuditDef{
AttachedResource: coretypes.ResourceMetaResourceSpanMapper,
AttachedResourceID: handler.ResponseJSONPath("data.id"),
TargetResource: coretypes.ResourceMetaResourceSpanMapperGroup,
TargetResourceID: handler.PathParam("groupId"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryConfigurationChange,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -143,6 +179,12 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceSpanMapper,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("mapperId"),
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
@@ -163,6 +205,12 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceSpanMapper,
Verb: coretypes.VerbDelete,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("mapperId"),
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,7 +5,9 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
)
@@ -111,20 +113,28 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/me", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.UpdateMyUser), handler.OpenAPIDef{
ID: "UpdateMyUserV2",
Tags: []string{"users"},
Summary: "Update my user v2",
Description: "This endpoint updates the user I belong to",
Request: new(types.UpdatableUser),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/users/me", handler.New(
provider.authzMiddleware.OpenAccess(provider.userHandler.UpdateMyUser),
handler.OpenAPIDef{
ID: "UpdateMyUserV2",
Tags: []string{"users"},
Summary: "Update my user v2",
Description: "This endpoint updates the user I belong to",
Request: new(types.UpdatableUser),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{{Name: authtypes.IdentNProviderTokenizer.StringValue()}},
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceUser,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -179,20 +189,29 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser), handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
Summary: "Update user v2",
Description: "This endpoint updates the user by id",
Request: new(types.UpdatableUser),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/users/{id}", handler.New(
provider.authzMiddleware.AdminAccess(provider.userHandler.UpdateUser),
handler.OpenAPIDef{
ID: "UpdateUser",
Tags: []string{"users"},
Summary: "Update user v2",
Description: "This endpoint updates the user by id",
Request: new(types.UpdatableUser),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceUser,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryConfigurationChange,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -247,20 +266,29 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.CreateResetPasswordToken), handler.OpenAPIDef{
ID: "CreateResetPasswordToken",
Tags: []string{"users"},
Summary: "Create or regenerate reset password token for a user",
Description: "This endpoint creates or regenerates a reset password token for a user. If a valid token exists, it is returned. If expired, a new one is created.",
Request: nil,
RequestContentType: "",
Response: new(types.ResetPasswordToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/users/{id}/reset_password_tokens", handler.New(
provider.authzMiddleware.AdminAccess(provider.userHandler.CreateResetPasswordToken),
handler.OpenAPIDef{
ID: "CreateResetPasswordToken",
Tags: []string{"users"},
Summary: "Create or regenerate reset password token for a user",
Description: "This endpoint creates or regenerates a reset password token for a user. If a valid token exists, it is returned. If expired, a new one is created.",
Request: nil,
RequestContentType: "",
Response: new(types.ResetPasswordToken),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceFactorPassword,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
ResourceID: handler.PathParam("id"),
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -281,37 +309,53 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ChangePassword), handler.OpenAPIDef{
ID: "UpdateMyPassword",
Tags: []string{"users"},
Summary: "Updates my password",
Description: "This endpoint updates the password of the user I belong to",
Request: new(types.ChangePasswordRequest),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/users/me/factor_password", handler.New(
provider.authzMiddleware.OpenAccess(provider.userHandler.ChangePassword),
handler.OpenAPIDef{
ID: "UpdateMyPassword",
Tags: []string{"users"},
Summary: "Updates my password",
Description: "This endpoint updates the password of the user I belong to",
Request: new(types.ChangePasswordRequest),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceFactorPassword,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(provider.authzMiddleware.OpenAccess(provider.userHandler.ForgotPassword), handler.OpenAPIDef{
ID: "ForgotPassword",
Tags: []string{"users"},
Summary: "Forgot password",
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
Request: new(types.PostableForgotPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnprocessableEntity},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(
provider.authzMiddleware.OpenAccess(provider.userHandler.ForgotPassword),
handler.OpenAPIDef{
ID: "ForgotPassword",
Tags: []string{"users"},
Summary: "Forgot password",
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
Request: new(types.PostableForgotPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnprocessableEntity},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceFactorPassword,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
@@ -332,37 +376,59 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.SetRoleByUserID), handler.OpenAPIDef{
ID: "SetRoleByUserID",
Tags: []string{"users"},
Summary: "Set user roles",
Description: "This endpoint assigns the role to the user roles by user id",
Request: new(types.PostableRole),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/users/{id}/roles", handler.New(
provider.authzMiddleware.AdminAccess(provider.userHandler.SetRoleByUserID),
handler.OpenAPIDef{
ID: "SetRoleByUserID",
Tags: []string{"users"},
Summary: "Set user roles",
Description: "This endpoint assigns the role to the user roles by user id",
Request: new(types.PostableRole),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.AttachAuditDef{
AttachedResource: coretypes.ResourceRole,
AttachedResourceID: handler.BodyJSONPath("name"),
TargetResource: coretypes.ResourceUser,
TargetResourceID: handler.PathParam("id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(provider.authzMiddleware.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID), handler.OpenAPIDef{
ID: "RemoveUserRoleByUserIDAndRoleID",
Tags: []string{"users"},
Summary: "Remove a role from user",
Description: "This endpoint removes a role from the user by user id and role id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v2/users/{id}/roles/{roleId}", handler.New(
provider.authzMiddleware.AdminAccess(provider.userHandler.RemoveUserRoleByRoleID),
handler.OpenAPIDef{
ID: "RemoveUserRoleByUserIDAndRoleID",
Tags: []string{"users"},
Summary: "Remove a role from user",
Description: "This endpoint removes a role from the user by user id and role id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.AttachAuditDef{
AttachedResource: coretypes.ResourceRole,
AttachedResourceID: handler.PathParam("roleId"),
TargetResource: coretypes.ResourceUser,
TargetResourceID: handler.PathParam("id"),
Verb: coretypes.VerbAttach,
Category: audittypes.ActionCategoryAccessControl,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -5,25 +5,35 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/gorilla/mux"
)
func (provider *provider) addZeusRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/zeus/profiles", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutProfile), handler.OpenAPIDef{
ID: "PutProfile",
Tags: []string{"zeus"},
Summary: "Put profile in Zeus for a deployment.",
Description: "This endpoint saves the profile of a deployment to zeus.",
Request: new(zeustypes.PostableProfile),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/zeus/profiles", handler.New(
provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutProfile),
handler.OpenAPIDef{
ID: "PutProfile",
Tags: []string{"zeus"},
Summary: "Put profile in Zeus for a deployment.",
Description: "This endpoint saves the profile of a deployment to zeus.",
Request: new(zeustypes.PostableProfile),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceZeusProfile,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategorySystemEvent,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
@@ -44,20 +54,28 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost), handler.OpenAPIDef{
ID: "PutHost",
Tags: []string{"zeus"},
Summary: "Put host in Zeus for a deployment.",
Description: "This endpoint saves the host of a deployment to zeus.",
Request: new(zeustypes.PostableHost),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/zeus/hosts", handler.New(
provider.authzMiddleware.AdminAccess(provider.zeusHandler.PutHost),
handler.OpenAPIDef{
ID: "PutHost",
Tags: []string{"zeus"},
Summary: "Put host in Zeus for a deployment.",
Description: "This endpoint saves the host of a deployment to zeus.",
Request: new(zeustypes.PostableHost),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
handler.WithAuditDef(handler.BasicAuditDef{
Resource: coretypes.ResourceMetaResourceZeusHost,
Verb: coretypes.VerbUpdate,
Category: audittypes.ActionCategorySystemEvent,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}

View File

@@ -1,224 +0,0 @@
package auditorserver
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestSettings() factory.ScopedProviderSettings {
return factory.NewScopedProviderSettings(instrumentationtest.New().ToProviderSettings(), "auditorserver_test")
}
func newTestEvent(resource string, action coretypes.Verb) audittypes.AuditEvent {
return audittypes.AuditEvent{
Timestamp: time.Now(),
EventName: audittypes.NewEventName(coretypes.MustNewKind(resource), action),
AuditAttributes: audittypes.AuditAttributes{
Action: action,
Outcome: audittypes.OutcomeSuccess,
},
ResourceAttributes: audittypes.ResourceAttributes{
ResourceKind: coretypes.MustNewKind(resource),
},
}
}
func TestNew(t *testing.T) {
settings := newTestSettings()
config := Config{BufferSize: 10, BatchSize: 5, FlushInterval: time.Second}
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil })
require.NoError(t, err)
assert.NotNil(t, server)
}
func TestStart_Stop(t *testing.T) {
settings := newTestSettings()
config := Config{BufferSize: 10, BatchSize: 5, FlushInterval: time.Second}
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil })
require.NoError(t, err)
done := make(chan error, 1)
go func() { done <- server.Start(context.Background()) }()
require.NoError(t, server.Stop(context.Background()))
select {
case err := <-done:
assert.NoError(t, err)
case <-time.After(2 * time.Second):
assert.Fail(t, "Start did not return after Stop")
}
}
func TestAdd_FlushesOnBatchSize(t *testing.T) {
var exported []audittypes.AuditEvent
var mu sync.Mutex
settings := newTestSettings()
config := Config{BufferSize: 100, BatchSize: 3, FlushInterval: time.Hour}
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
mu.Lock()
exported = append(exported, events...)
mu.Unlock()
return nil
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = server.Start(ctx) }()
for i := 0; i < 3; i++ {
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
}
assert.Eventually(t, func() bool {
mu.Lock()
defer mu.Unlock()
return len(exported) == 3
}, 2*time.Second, 10*time.Millisecond)
require.NoError(t, server.Stop(ctx))
}
func TestAdd_FlushesOnInterval(t *testing.T) {
var exported atomic.Int64
settings := newTestSettings()
config := Config{BufferSize: 100, BatchSize: 1000, FlushInterval: 50 * time.Millisecond}
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
exported.Add(int64(len(events)))
return nil
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = server.Start(ctx) }()
server.Add(ctx, newTestEvent("user", coretypes.VerbUpdate))
assert.Eventually(t, func() bool {
return exported.Load() == 1
}, 2*time.Second, 10*time.Millisecond)
require.NoError(t, server.Stop(ctx))
}
func TestAdd_DropsWhenBufferFull(t *testing.T) {
settings := newTestSettings()
config := Config{BufferSize: 2, BatchSize: 100, FlushInterval: time.Hour}
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil })
require.NoError(t, err)
ctx := context.Background()
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbUpdate))
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbDelete))
assert.Equal(t, 2, server.queueLen())
}
func TestStop_DrainsRemainingEvents(t *testing.T) {
var exported atomic.Int64
settings := newTestSettings()
config := Config{BufferSize: 100, BatchSize: 100, FlushInterval: time.Hour}
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
exported.Add(int64(len(events)))
return nil
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = server.Start(ctx) }()
for i := 0; i < 5; i++ {
server.Add(ctx, newTestEvent("alert-rule", coretypes.VerbCreate))
}
require.NoError(t, server.Stop(ctx))
assert.Equal(t, int64(5), exported.Load())
}
func TestAdd_ContinuesAfterExportFailure(t *testing.T) {
var calls atomic.Int64
settings := newTestSettings()
config := Config{BufferSize: 100, BatchSize: 2, FlushInterval: time.Hour}
server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error {
calls.Add(1)
return errors.New(errors.TypeInternal, errors.CodeInternal, "connection refused")
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = server.Start(ctx) }()
server.Add(ctx, newTestEvent("user", coretypes.VerbDelete))
server.Add(ctx, newTestEvent("user", coretypes.VerbDelete))
assert.Eventually(t, func() bool {
return calls.Load() >= 1
}, 2*time.Second, 10*time.Millisecond)
require.NoError(t, server.Stop(ctx))
}
func TestAdd_ConcurrentSafety(t *testing.T) {
var exported atomic.Int64
settings := newTestSettings()
config := Config{BufferSize: 1000, BatchSize: 10, FlushInterval: 50 * time.Millisecond}
server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error {
exported.Add(int64(len(events)))
return nil
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = server.Start(ctx) }()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
server.Add(ctx, newTestEvent("dashboard", coretypes.VerbCreate))
}()
}
wg.Wait()
require.NoError(t, server.Stop(ctx))
assert.Equal(t, int64(100), exported.Load())
}

View File

@@ -25,6 +25,8 @@ type Config struct {
FlushInterval time.Duration `mapstructure:"flush_interval"`
OTLPHTTP OTLPHTTPConfig `mapstructure:"otlphttp"`
File FileConfig `mapstructure:"file"`
}
// OTLPHTTPConfig holds configuration for the OTLP HTTP exporter provider.
@@ -46,6 +48,12 @@ type OTLPHTTPConfig struct {
Retry RetryConfig `mapstructure:"retry"`
}
type FileConfig struct {
// Path is the absolute path to the audit log file. The file is opened with
// O_APPEND|O_CREATE|O_WRONLY; existing contents are preserved across runs.
Path string `mapstructure:"path"`
}
// RetryConfig configures exponential backoff for the OTLP HTTP exporter.
type RetryConfig struct {
// Enabled controls whether retries are attempted on transient failures.
@@ -111,5 +119,11 @@ func (c Config) Validate() error {
}
}
if c.Provider == "file" {
if c.File.Path == "" {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::file::path must be set when provider is file")
}
}
return nil
}

View File

@@ -15,13 +15,13 @@ type ServeOpenAPIFunc func(openapi.OperationContext)
type Handler interface {
http.Handler
ServeOpenAPI(openapi.OperationContext)
AuditDef() *AuditDef
AuditDefs() []AuditDef
}
type handler struct {
handlerFunc http.HandlerFunc
openAPIDef OpenAPIDef
auditDef *AuditDef
auditDefs []AuditDef
}
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Handler {
@@ -130,6 +130,6 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
}
}
func (handler *handler) AuditDef() *AuditDef {
return handler.auditDef
func (handler *handler) AuditDefs() []AuditDef {
return handler.auditDefs
}

View File

@@ -1,25 +1,139 @@
package handler
import (
"net/http"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
"github.com/tidwall/gjson"
)
// Option configures optional behaviour on a handler created by New.
type Option func(*handler)
type AuditDef struct {
ResourceKind coretypes.Kind // Typeable.Kind() value, e.g. "dashboard", "user".
Action coretypes.Verb // create, update, delete, etc.
Category audittypes.ActionCategory // access_control, configuration_change, etc.
ResourceIDParam string // Gorilla mux path param name for the resource ID.
// ExtractorContext carries everything a ResourceIDExtractor might read out of
// a request/response cycle. The middleware pre-buffers the request body and
// captures the response body so post-handler extraction works on both sides.
type ExtractorContext struct {
Request *http.Request
RequestBody []byte
ResponseBody []byte
}
// WithAudit attaches an AuditDef to the handler. The actual audit event
// emission is handled by the middleware layer, which reads the AuditDef
// from the matched route's handler.
func WithAuditDef(def AuditDef) Option {
return func(h *handler) {
h.auditDef = &def
// ResourceIDExtractor pulls a resource id from an incoming request and/or its
// response. Returns an empty string with no error when the id source is
// genuinely absent (e.g. "me" routes that act on the caller without an id).
type ResourceIDExtractor func(ExtractorContext) (string, error)
// ResourceIDsExtractor pulls a list of resource ids. Used by AttachManyAuditDef
// to fan out one audit event per attached entity referenced in a request body.
type ResourceIDsExtractor func(ExtractorContext) ([]string, error)
// PathParam returns an extractor that reads a Gorilla mux path variable.
func PathParam(name string) ResourceIDExtractor {
return func(ctx ExtractorContext) (string, error) {
vars := mux.Vars(ctx.Request)
if vars == nil {
return "", nil
}
return vars[name], nil
}
}
// BodyJSONPath returns an extractor that reads a JSON path from the request
// body via gjson. The middleware buffers the request body before forwarding
// to the handler, so this extractor still works after the handler runs.
func BodyJSONPath(path string) ResourceIDExtractor {
return func(ctx ExtractorContext) (string, error) {
return gjson.GetBytes(ctx.RequestBody, path).String(), nil
}
}
// ResponseJSONPath returns an extractor that reads a JSON path from the
// response body via gjson. Useful for Create routes where the new resource id
// is only known after the handler runs and writes the response payload.
func ResponseJSONPath(path string) ResourceIDExtractor {
return func(ctx ExtractorContext) (string, error) {
return gjson.GetBytes(ctx.ResponseBody, path).String(), nil
}
}
// BodyJSONArray returns a multi-id extractor that reads a JSON array of
// strings out of the request body at the given gjson path.
func BodyJSONArray(path string) ResourceIDsExtractor {
return func(ctx ExtractorContext) ([]string, error) {
result := gjson.GetBytes(ctx.RequestBody, path)
if !result.Exists() {
return nil, nil
}
array := result.Array()
ids := make([]string, 0, len(array))
for _, r := range array {
ids = append(ids, r.String())
}
return ids, nil
}
}
// AuditDef is a sealed interface implemented by BasicAuditDef and
// AttachAuditDef. The middleware type-switches over its implementations to
// build the audit event for the matched route.
type AuditDef interface {
sealAuditDef()
}
// BasicAuditDef declares audit metadata for routes that operate on a single
// resource. EventName is derived as Resource.Kind() + "." + Verb.PastTense().
type BasicAuditDef struct {
Resource coretypes.Resource
Verb coretypes.Verb
Category audittypes.ActionCategory
ResourceID ResourceIDExtractor // nil for collection routes with no addressable id
}
func (BasicAuditDef) sealAuditDef() {}
// AttachAuditDef declares audit metadata for routes that attach one resource
// to another (e.g. role attached to a user). The event subject is the
// attached resource; the target carries where it was attached. EventName is
// derived as AttachedResource.Kind() + "." + Verb.PastTense().
type AttachAuditDef struct {
AttachedResource coretypes.Resource
AttachedResourceID ResourceIDExtractor
TargetResource coretypes.Resource
TargetResourceID ResourceIDExtractor
Verb coretypes.Verb
Category audittypes.ActionCategory
}
func (AttachAuditDef) sealAuditDef() {}
// AttachManyAuditDef declares that a single request attaches many of the same
// kind of resource to one target. The middleware fans out one attach event per
// id returned by AttachedResourceIDs. Used for routes whose body carries a
// list of references (e.g. rule preferredChannels, planned-maintenance
// alertIds, route-policy channels).
type AttachManyAuditDef struct {
AttachedResource coretypes.Resource
AttachedResourceIDs ResourceIDsExtractor
TargetResource coretypes.Resource
TargetResourceID ResourceIDExtractor
Verb coretypes.Verb
Category audittypes.ActionCategory
}
func (AttachManyAuditDef) sealAuditDef() {}
// WithAuditDef attaches one or more AuditDef declarations to the handler. A
// single route can produce multiple audit events — e.g. creating a resource
// that is simultaneously attached to a parent emits one BasicAuditDef and one
// AttachAuditDef. The middleware emits one event per def in declaration order.
func WithAuditDef(defs ...AuditDef) Option {
return func(h *handler) {
h.auditDefs = append(h.auditDefs, defs...)
}
}

View File

@@ -1,6 +1,8 @@
package middleware
import (
"bytes"
"io"
"log/slog"
"net"
"net/http"
@@ -16,6 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
)
const (
@@ -59,6 +62,14 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
string(semconv.HTTPRouteKey), path,
}
// Pre-buffer the request body if the route declares any AuditDefs that
// might want to extract from it after the handler has consumed the body.
var requestBody []byte
if len(auditDefsFromRequest(req)) > 0 && req.Body != nil {
requestBody, _ = io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewReader(requestBody))
}
responseBuffer := &byteBuffer{}
writer := newResponseCapture(rw, responseBuffer)
next.ServeHTTP(writer, req)
@@ -70,7 +81,7 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
return
}
middleware.emitAuditEvent(req, writer, path)
middleware.emitAuditEvent(req, writer, path, requestBody)
fields = append(fields,
string(semconv.HTTPResponseStatusCodeKey), statusCode,
@@ -89,51 +100,66 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
})
}
func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string) {
func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string, requestBody []byte) {
if middleware.auditor == nil {
return
}
def := auditDefFromRequest(req)
if def == nil {
defs := auditDefsFromRequest(req)
if len(defs) == 0 {
return
}
// extract claims
claims, _ := authtypes.ClaimsFromContext(req.Context())
// extract status code
statusCode := writer.StatusCode()
// extract traces.
span := trace.SpanFromContext(req.Context())
// extract error details.
var errorType, errorCode string
if statusCode >= 400 {
errorType = render.ErrorTypeFromStatusCode(statusCode)
errorCode = render.ErrorCodeFromBody(writer.BodyBytes())
}
event := audittypes.NewAuditEventFromHTTPRequest(
req,
routeTemplate,
statusCode,
span.SpanContext().TraceID(),
span.SpanContext().SpanID(),
def.Action,
def.Category,
claims,
resourceIDFromRequest(req, def.ResourceIDParam),
def.ResourceKind,
errorType,
errorCode,
)
extractorCtx := handler.ExtractorContext{
Request: req,
RequestBody: requestBody,
ResponseBody: writer.BodyBytes(),
}
middleware.auditor.Audit(req.Context(), event)
for _, def := range defs {
resolved, err := resolveAuditDef(extractorCtx, def)
if err != nil {
middleware.logger.WarnContext(req.Context(), "audit event dropped — resource id extraction failed", errors.Attr(err))
continue
}
for _, r := range resolved {
event := audittypes.NewAuditEventFromHTTPRequest(
req,
routeTemplate,
statusCode,
span.SpanContext().TraceID(),
span.SpanContext().SpanID(),
r.Verb,
r.Category,
claims,
r.ResourceAttributes,
errorType,
errorCode,
)
middleware.auditor.Audit(req.Context(), event)
}
}
}
func auditDefFromRequest(req *http.Request) *handler.AuditDef {
type resolvedAuditDef struct {
Verb coretypes.Verb
Category audittypes.ActionCategory
ResourceAttributes audittypes.ResourceAttributes
}
func auditDefsFromRequest(req *http.Request) []handler.AuditDef {
route := mux.CurrentRoute(req)
if route == nil {
return nil
@@ -152,18 +178,76 @@ func auditDefFromRequest(req *http.Request) *handler.AuditDef {
return nil
}
return provider.AuditDef()
return provider.AuditDefs()
}
func resourceIDFromRequest(req *http.Request, param string) string {
if param == "" {
return ""
func resolveAuditDef(ctx handler.ExtractorContext, def handler.AuditDef) ([]resolvedAuditDef, error) {
switch d := def.(type) {
case handler.BasicAuditDef:
resourceID, err := extractResourceID(ctx, d.ResourceID)
if err != nil {
return nil, err
}
return []resolvedAuditDef{{
Verb: d.Verb,
Category: d.Category,
ResourceAttributes: audittypes.NewResourceAttributes(d.Resource, resourceID),
}}, nil
case handler.AttachAuditDef:
attachedID, err := extractResourceID(ctx, d.AttachedResourceID)
if err != nil {
return nil, err
}
targetID, err := extractResourceID(ctx, d.TargetResourceID)
if err != nil {
return nil, err
}
return []resolvedAuditDef{{
Verb: d.Verb,
Category: d.Category,
ResourceAttributes: audittypes.NewAttachResourceAttributes(d.AttachedResource, attachedID, d.TargetResource, targetID),
}}, nil
case handler.AttachManyAuditDef:
ids, err := extractResourceIDs(ctx, d.AttachedResourceIDs)
if err != nil {
return nil, err
}
targetID, err := extractResourceID(ctx, d.TargetResourceID)
if err != nil {
return nil, err
}
resolved := make([]resolvedAuditDef, 0, len(ids))
for _, id := range ids {
resolved = append(resolved, resolvedAuditDef{
Verb: d.Verb,
Category: d.Category,
ResourceAttributes: audittypes.NewAttachResourceAttributes(d.AttachedResource, id, d.TargetResource, targetID),
})
}
return resolved, nil
}
vars := mux.Vars(req)
if vars == nil {
return ""
}
return vars[param]
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "unknown AuditDef implementation %T", def)
}
func extractResourceID(ctx handler.ExtractorContext, extractor handler.ResourceIDExtractor) (string, error) {
if extractor == nil {
return "", nil
}
return extractor(ctx)
}
func extractResourceIDs(ctx handler.ExtractorContext, extractor handler.ResourceIDsExtractor) ([]string, error) {
if extractor == nil {
return nil, nil
}
return extractor(ctx)
}

View File

@@ -1343,7 +1343,7 @@ func getLocalTableName(tableName string) string {
}
func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
@@ -1434,10 +1434,6 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, userID
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
@@ -1515,7 +1511,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, userID
return &model.SetTTLResponseItem{Message: "move ttl has been successfully set up"}, nil
}
func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalTraces.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
@@ -1576,10 +1572,6 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, userI
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
@@ -1695,7 +1687,7 @@ func (r *ClickHouseReader) hasCustomRetentionColumn(ctx context.Context) (bool,
return true, nil
}
func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, userID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error) {
func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
@@ -1726,7 +1718,7 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, userID st
ttlParams.ToColdStorageDuration = 0
}
ttlResult, apiErr := r.SetTTL(ctx, orgID, userID, ttlParams)
ttlResult, apiErr := r.SetTTL(ctx, orgID, ttlParams)
if apiErr != nil {
return nil, errorsV2.Wrapf(apiErr.Err, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to set standard TTL")
}
@@ -1855,10 +1847,6 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, userID st
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: params.DefaultTTLDays,
@@ -2197,24 +2185,24 @@ func (r *ClickHouseReader) validateTTLConditions(ctx context.Context, ttlConditi
// SetTTL sets the TTL for traces or metrics or logs tables.
// This is an async API which creates goroutines to set TTL.
// Status of TTL update is tracked with ttl_status table in sqlite db.
func (r *ClickHouseReader) SetTTL(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) SetTTL(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
// Keep only latest 100 transactions/requests
r.deleteTtlTransactions(ctx, orgID, 100)
switch params.Type {
case constants.TraceTTL:
return r.setTTLTraces(ctx, orgID, userID, params)
return r.setTTLTraces(ctx, orgID, params)
case constants.MetricsTTL:
return r.setTTLMetrics(ctx, orgID, userID, params)
return r.setTTLMetrics(ctx, orgID, params)
case constants.LogsTTL:
return r.setTTLLogs(ctx, orgID, userID, params)
return r.setTTLLogs(ctx, orgID, params)
default:
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while setting ttl. ttl type should be <metrics|traces>, got %v", params.Type)}
}
}
func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalMetrics.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
@@ -2256,10 +2244,6 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, user
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),

View File

@@ -1655,7 +1655,7 @@ func (aH *APIHandler) setTTL(w http.ResponseWriter, r *http.Request) {
}
// Context is not used here as TTL is long duration DB operation
result, apiErr := aH.reader.SetTTL(context.Background(), claims.OrgID, claims.UserID, ttlParams)
result, apiErr := aH.reader.SetTTL(context.Background(), claims.OrgID, ttlParams)
if apiErr != nil {
if apiErr.Typ == model.ErrorConflict {
aH.HandleError(w, apiErr.Err, http.StatusConflict)
@@ -1684,7 +1684,7 @@ func (aH *APIHandler) setCustomRetentionTTL(w http.ResponseWriter, r *http.Reque
}
// Context is not used here as TTL is long duration DB operation
result, apiErr := aH.reader.SetTTLV2(context.Background(), claims.OrgID, claims.UserID, &params)
result, apiErr := aH.reader.SetTTLV2(context.Background(), claims.OrgID, &params)
if apiErr != nil {
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInternal, apiErr.Error()))
return

View File

@@ -46,8 +46,8 @@ type Reader interface {
GetFlamegraphSpansForTrace(ctx context.Context, orgID valuer.UUID, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, error)
// Setter Interfaces
SetTTL(ctx context.Context, orgID string, userID string, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError)
SetTTLV2(ctx context.Context, orgID string, userID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error)
SetTTL(ctx context.Context, orgID string, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError)
SetTTLV2(ctx context.Context, orgID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error)
FetchTemporality(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]map[v3.Temporality]bool, error)
GetMetricAggregateAttributes(ctx context.Context, orgID valuer.UUID, req *v3.AggregateAttributeRequest, skipSignozMetrics bool) (*v3.AggregateAttributeResponse, error)

View File

@@ -196,7 +196,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
sqlmigration.NewUpdateTTLSettingUserAuditFactory(sqlstore, sqlschema),
)
}

View File

@@ -1,84 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateTTLSettingUserAudit struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewUpdateTTLSettingUserAuditFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("update_ttl_setting_user_audit"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newUpdateTTLSettingUserAudit(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newUpdateTTLSettingUserAudit(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &updateTTLSettingUserAudit{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *updateTTLSettingUserAudit) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateTTLSettingUserAudit) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("ttl_setting"))
if err != nil {
return err
}
columns := []*sqlschema.Column{
{
Name: sqlschema.ColumnName("created_by"),
DataType: sqlschema.DataTypeText,
Nullable: true,
},
{
Name: sqlschema.ColumnName("updated_by"),
DataType: sqlschema.DataTypeText,
Nullable: true,
},
}
for _, column := range columns {
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, nil)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *updateTTLSettingUserAudit) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -5,12 +5,12 @@ import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/stretchr/testify/require"
)
@@ -32,7 +32,7 @@ func auditFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
return map[string][]*telemetrytypes.TelemetryFieldKey{
"service.name": {key("service.name", res, str, false)},
"signoz.audit.action": {key("signoz.audit.action", attr, str, true)},
"signoz.audit.verb": {key("signoz.audit.verb", attr, str, true)},
"signoz.audit.outcome": {key("signoz.audit.outcome", attr, str, true)},
"signoz.audit.principal.email": {key("signoz.audit.principal.email", attr, str, true)},
"signoz.audit.principal.id": {key("signoz.audit.principal.id", attr, str, true)},
@@ -131,20 +131,20 @@ func TestStatementBuilder(t *testing.T) {
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.audit.resource.id%", "%signoz.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
},
},
// List: all dashboard deletions (compliance — resource.kind + action AND)
// List: all dashboard deletions (compliance — resource.kind + verb AND)
{
name: "ListByResourceKindAndAction",
name: "ListByResourceKindAndVerb",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Source: telemetrytypes.SourceAudit,
Filter: &qbtypes.Filter{
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
},
Limit: 100,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
},
},
@@ -165,23 +165,23 @@ func TestStatementBuilder(t *testing.T) {
Args: []any{"service_account", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
},
},
// Scalar: alert — count forbidden errors (outcome + action AND)
// Scalar: alert — count forbidden errors (outcome + verb AND)
{
name: "ScalarCountByOutcomeAndAction",
name: "ScalarCountByOutcomeAndVerb",
requestType: qbtypes.RequestTypeScalar,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Source: telemetrytypes.SourceAudit,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Filter: &qbtypes.Filter{
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.action = 'update'",
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.verb = 'update'",
},
Aggregations: []qbtypes.LogAggregation{
{Expression: "count()"},
},
},
expected: qbtypes.Statement{
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = ?) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = ?) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
Args: []any{"failure", true, "update", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
},

View File

@@ -11,22 +11,22 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// Audit attributes — Action (What).
// Audit attributes — Verb (What).
type AuditAttributes struct {
Action coretypes.Verb // guaranteed to be present
Verb coretypes.Verb // guaranteed to be present
ActionCategory ActionCategory // guaranteed to be present
Outcome Outcome // guaranteed to be present
IdentNProvider authtypes.IdentNProvider
}
func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category ActionCategory, claims authtypes.Claims) AuditAttributes {
func NewAuditAttributesFromHTTP(statusCode int, verb coretypes.Verb, category ActionCategory, claims authtypes.Claims) AuditAttributes {
outcome := OutcomeFailure
if statusCode >= 200 && statusCode < 400 {
outcome = OutcomeSuccess
}
return AuditAttributes{
Action: action,
Verb: verb,
ActionCategory: category,
Outcome: outcome,
IdentNProvider: claims.IdentNProvider,
@@ -34,7 +34,7 @@ func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category
}
func (attributes AuditAttributes) Put(dest pcommon.Map) {
dest.PutStr("signoz.audit.action", attributes.Action.StringValue())
dest.PutStr("signoz.audit.verb", attributes.Verb.StringValue())
dest.PutStr("signoz.audit.action_category", attributes.ActionCategory.StringValue())
dest.PutStr("signoz.audit.outcome", attributes.Outcome.StringValue())
putStrIfNotEmpty(dest, "signoz.audit.identn_provider", attributes.IdentNProvider.StringValue())
@@ -70,24 +70,47 @@ func (attributes PrincipalAttributes) Put(dest pcommon.Map) {
// Audit attributes — Resource (On What).
// These are OTel resource attributes (placed on the Resource, not event attributes).
// For attach events, Target carries the resource the primary was attached to.
type ResourceAttributes struct {
ResourceID string
ResourceKind coretypes.Kind // guaranteed to be present
Resource coretypes.Resource // guaranteed to be present
ResourceID string
TargetResource coretypes.Resource // present only for attach events
TargetResourceID string
}
func NewResourceAttributes(resourceID string, resourceKind coretypes.Kind) ResourceAttributes {
func NewResourceAttributes(resource coretypes.Resource, resourceID string) ResourceAttributes {
return ResourceAttributes{
ResourceID: resourceID,
ResourceKind: resourceKind,
Resource: resource,
ResourceID: resourceID,
}
}
func NewAttachResourceAttributes(attachedResource coretypes.Resource, attachedResourceID string, targetResource coretypes.Resource, targetResourceID string) ResourceAttributes {
return ResourceAttributes{
Resource: attachedResource,
ResourceID: attachedResourceID,
TargetResource: targetResource,
TargetResourceID: targetResourceID,
}
}
// PutResource writes the resource attributes to an OTel Resource's attribute map.
// These are resource-level attributes (stored in the resource JSON column),
// not event-level attributes (stored in attributes_string).
func (attributes ResourceAttributes) PutResource(dest pcommon.Map) {
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.ResourceKind.String())
func (attributes ResourceAttributes) PutResource(dest pcommon.Map, orgID valuer.UUID) {
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.Resource.Kind().String())
putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID)
if attributes.ResourceID != "" {
dest.PutStr("signoz.audit.resource.object", attributes.Resource.Object(orgID, attributes.ResourceID))
}
if attributes.TargetResource != nil {
putStrIfNotEmpty(dest, "signoz.audit.target.kind", attributes.TargetResource.Kind().String())
putStrIfNotEmpty(dest, "signoz.audit.target.id", attributes.TargetResourceID)
if attributes.TargetResourceID != "" {
dest.PutStr("signoz.audit.target.object", attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
}
}
}
// Audit attributes — Error (When outcome is failure)
@@ -180,26 +203,37 @@ func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttri
b.WriteString(principalAttributes.PrincipalID.StringValue())
}
// Action: " created" or " failed to create".
// Verb: " created" or " failed to create".
if b.Len() > 0 {
b.WriteString(" ")
}
if auditAttributes.Outcome == OutcomeSuccess {
b.WriteString(auditAttributes.Action.PastTense())
b.WriteString(auditAttributes.Verb.PastTense())
} else {
b.WriteString("failed to ")
b.WriteString(auditAttributes.Action.StringValue())
b.WriteString(auditAttributes.Verb.StringValue())
}
// Resource: " kind (id)" or " kind".
b.WriteString(" ")
b.WriteString(resourceAttributes.ResourceKind.String())
b.WriteString(resourceAttributes.Resource.Kind().String())
if resourceAttributes.ResourceID != "" {
b.WriteString(" (")
b.WriteString(resourceAttributes.ResourceID)
b.WriteString(")")
}
// Target (attach events): " to kind (id)" or " to kind".
if resourceAttributes.TargetResource != nil {
b.WriteString(" to ")
b.WriteString(resourceAttributes.TargetResource.Kind().String())
if resourceAttributes.TargetResourceID != "" {
b.WriteString(" (")
b.WriteString(resourceAttributes.TargetResourceID)
b.WriteString(")")
}
}
// Error suffix (failure only): ": type (code)" or ": type" or ": (code)" or omitted.
if auditAttributes.Outcome == OutcomeFailure {
errorType := errorAttributes.ErrorType

View File

@@ -1,204 +0,0 @@
package audittypes
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
)
func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) {
claims := authtypes.Claims{IdentNProvider: authtypes.IdentNProviderTokenizer}
testCases := []struct {
name string
statusCode int
expectedOutcome Outcome
}{
{
name: "200_Success",
statusCode: 200,
expectedOutcome: OutcomeSuccess,
},
{
name: "399_Success",
statusCode: 399,
expectedOutcome: OutcomeSuccess,
},
{
name: "400_Failure",
statusCode: 400,
expectedOutcome: OutcomeFailure,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
attrs := NewAuditAttributesFromHTTP(testCase.statusCode, coretypes.VerbUpdate, ActionCategoryConfigurationChange, claims)
assert.Equal(t, testCase.expectedOutcome, attrs.Outcome)
})
}
}
func TestNewBody(t *testing.T) {
testCases := []struct {
name string
auditAttributes AuditAttributes
principalAttributes PrincipalAttributes
resourceAttributes ResourceAttributes
errorAttributes ErrorAttributes
expectedBody string
}{
{
name: "Success_EmptyResourceID",
auditAttributes: AuditAttributes{
Action: coretypes.VerbDelete,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "",
ResourceKind: coretypes.MustNewKind("dashboard"),
},
errorAttributes: ErrorAttributes{},
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) deleted dashboard",
},
{
name: "Success_EmptyPrincipalEmail",
auditAttributes: AuditAttributes{
Action: coretypes.VerbDelete,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
PrincipalEmail: valuer.Email{},
},
resourceAttributes: ResourceAttributes{
ResourceID: "abd",
ResourceKind: coretypes.MustNewKind("dashboard"),
},
errorAttributes: ErrorAttributes{},
expectedBody: "019a1234-abcd-7000-8000-567800000001 deleted dashboard (abd)",
},
{
name: "Success_EmptyPrincipalIDandEmail",
auditAttributes: AuditAttributes{
Action: coretypes.VerbDelete,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.UUID{},
PrincipalEmail: valuer.Email{},
},
resourceAttributes: ResourceAttributes{
ResourceID: "abd",
ResourceKind: coretypes.MustNewKind("dashboard"),
},
errorAttributes: ErrorAttributes{},
expectedBody: "deleted dashboard (abd)",
},
{
name: "Success_AllPresent",
auditAttributes: AuditAttributes{
Action: coretypes.VerbCreate,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
PrincipalEmail: valuer.MustNewEmail("alice@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "019b-5678",
ResourceKind: coretypes.MustNewKind("dashboard"),
},
errorAttributes: ErrorAttributes{},
expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678)",
},
{
name: "Success_EmptyEverythingOptional",
auditAttributes: AuditAttributes{
Action: coretypes.VerbUpdate,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
principalAttributes: PrincipalAttributes{},
resourceAttributes: ResourceAttributes{
ResourceKind: coretypes.MustNewKind("alert-rule"),
},
errorAttributes: ErrorAttributes{},
expectedBody: "updated alert-rule",
},
{
name: "Failure_AllPresent",
auditAttributes: AuditAttributes{
Action: coretypes.VerbUpdate,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeFailure,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.MustNewUUID("019aaaaa-bbbb-7000-8000-cccc00000002"),
PrincipalEmail: valuer.MustNewEmail("viewer@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "019b-5678",
ResourceKind: coretypes.MustNewKind("dashboard"),
},
errorAttributes: ErrorAttributes{
ErrorType: "forbidden",
ErrorCode: "authz_forbidden",
},
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden (authz_forbidden)",
},
{
name: "Failure_ErrorTypeOnly",
auditAttributes: AuditAttributes{
Action: coretypes.VerbDelete,
Outcome: OutcomeFailure,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceKind: coretypes.MustNewKind("user"),
},
errorAttributes: ErrorAttributes{
ErrorType: "not-found",
},
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to delete user: not-found",
},
{
name: "Failure_NoErrorDetails",
auditAttributes: AuditAttributes{
Action: coretypes.VerbCreate,
Outcome: OutcomeFailure,
},
principalAttributes: PrincipalAttributes{
PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"),
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "019b-5678",
ResourceKind: coretypes.MustNewKind("dashboard"),
},
errorAttributes: ErrorAttributes{},
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to create dashboard (019b-5678)",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
body := newBody(testCase.auditAttributes, testCase.principalAttributes, testCase.resourceAttributes, testCase.errorAttributes)
assert.Equal(t, testCase.expectedBody, body)
})
}
}

View File

@@ -28,7 +28,7 @@ type AuditEvent struct {
// OTel LogRecord Intrinsic
EventName EventName
// Custom Audit Attributes - Action
// Custom Audit Attributes - Verb
AuditAttributes AuditAttributes
// Custom Audit Attributes - Principal
@@ -50,17 +50,15 @@ func NewAuditEventFromHTTPRequest(
statusCode int,
traceID oteltrace.TraceID,
spanID oteltrace.SpanID,
action coretypes.Verb,
verb coretypes.Verb,
actionCategory ActionCategory,
claims authtypes.Claims,
resourceID string,
resourceKind coretypes.Kind,
resourceAttributes ResourceAttributes,
errorType string,
errorCode string,
) AuditEvent {
auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory, claims)
auditAttributes := NewAuditAttributesFromHTTP(statusCode, verb, actionCategory, claims)
principalAttributes := NewPrincipalAttributesFromClaims(claims)
resourceAttributes := NewResourceAttributes(resourceID, resourceKind)
errorAttributes := NewErrorAttributes(errorType, errorCode)
transportAttributes := NewTransportAttributesFromHTTP(req, route, statusCode)
@@ -69,7 +67,7 @@ func NewAuditEventFromHTTPRequest(
TraceID: traceID,
SpanID: spanID,
Body: newBody(auditAttributes, principalAttributes, resourceAttributes, errorAttributes),
EventName: NewEventName(resourceAttributes.ResourceKind, auditAttributes.Action),
EventName: NewEventName(resourceAttributes.Resource.Kind(), auditAttributes.Verb),
AuditAttributes: auditAttributes,
PrincipalAttributes: principalAttributes,
ResourceAttributes: resourceAttributes,
@@ -89,7 +87,7 @@ func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, s
groups := make(map[resourceKey][]int)
order := make([]resourceKey, 0)
for i, event := range events {
key := resourceKey{kind: event.ResourceAttributes.ResourceKind.String(), id: event.ResourceAttributes.ResourceID}
key := resourceKey{kind: event.ResourceAttributes.Resource.Kind().String(), id: event.ResourceAttributes.ResourceID}
if _, exists := groups[key]; !exists {
order = append(order, key)
}
@@ -101,7 +99,8 @@ func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, s
resourceAttrs := resourceLogs.Resource().Attributes()
resourceAttrs.PutStr(string(semconv.ServiceNameKey), name)
resourceAttrs.PutStr(string(semconv.ServiceVersionKey), version)
events[groups[key][0]].ResourceAttributes.PutResource(resourceAttrs)
head := events[groups[key][0]]
head.ResourceAttributes.PutResource(resourceAttrs, head.PrincipalAttributes.PrincipalOrgID)
scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
scopeLogs.Scope().SetName(scope)

View File

@@ -1,242 +0,0 @@
package audittypes
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/stretchr/testify/assert"
oteltrace "go.opentelemetry.io/otel/trace"
)
var (
testDashboardKind = coretypes.MustNewKind("dashboard")
)
func TestNewAuditEventFromHTTPRequest(t *testing.T) {
traceID := oteltrace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
spanID := oteltrace.SpanID{1, 2, 3, 4, 5, 6, 7, 8}
testCases := []struct {
name string
method string
path string
route string
statusCode int
action coretypes.Verb
category ActionCategory
claims authtypes.Claims
resourceID string
resourceKind coretypes.Kind
errorType string
errorCode string
expectedOutcome Outcome
expectedBody string
}{
{
name: "Success_DashboardCreated",
method: http.MethodPost,
path: "/api/v1/dashboards",
route: "/api/v1/dashboards",
statusCode: http.StatusOK,
action: coretypes.VerbCreate,
category: ActionCategoryConfigurationChange,
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Email: "alice@acme.com", OrgID: "019a-0000-0000-0001", IdentNProvider: authtypes.IdentNProviderTokenizer},
resourceID: "019b-5678-efgh-9012",
resourceKind: testDashboardKind,
expectedOutcome: OutcomeSuccess,
expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678-efgh-9012)",
},
{
name: "Failure_ForbiddenDashboardUpdate",
method: http.MethodPut,
path: "/api/v1/dashboards/019b-5678-efgh-9012",
route: "/api/v1/dashboards/{id}",
statusCode: http.StatusForbidden,
action: coretypes.VerbUpdate,
category: ActionCategoryConfigurationChange,
claims: authtypes.Claims{UserID: "019aaaaa-bbbb-7000-8000-cccc00000002", Email: "viewer@acme.com", OrgID: "019a-0000-0000-0001", IdentNProvider: authtypes.IdentNProviderTokenizer},
resourceID: "019b-5678-efgh-9012",
resourceKind: testDashboardKind,
errorType: "forbidden",
errorCode: "authz_forbidden",
expectedOutcome: OutcomeFailure,
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden (authz_forbidden)",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := httptest.NewRequest(testCase.method, testCase.path, nil)
event := NewAuditEventFromHTTPRequest(
req,
testCase.route,
testCase.statusCode,
traceID,
spanID,
testCase.action,
testCase.category,
testCase.claims,
testCase.resourceID,
testCase.resourceKind,
testCase.errorType,
testCase.errorCode,
)
assert.Equal(t, testCase.expectedOutcome, event.AuditAttributes.Outcome)
assert.Equal(t, testCase.expectedBody, event.Body)
assert.Equal(t, testCase.resourceKind, event.ResourceAttributes.ResourceKind)
assert.Equal(t, testCase.resourceID, event.ResourceAttributes.ResourceID)
assert.Equal(t, testCase.action, event.AuditAttributes.Action)
assert.Equal(t, testCase.category, event.AuditAttributes.ActionCategory)
assert.Equal(t, testCase.route, event.TransportAttributes.HTTPRoute)
assert.Equal(t, testCase.statusCode, event.TransportAttributes.HTTPStatusCode)
assert.Equal(t, testCase.method, event.TransportAttributes.HTTPMethod)
assert.Equal(t, traceID, event.TraceID)
assert.Equal(t, spanID, event.SpanID)
assert.Equal(t, testCase.errorType, event.ErrorAttributes.ErrorType)
assert.Equal(t, testCase.errorCode, event.ErrorAttributes.ErrorCode)
})
}
}
func newTestEvent(resourceKind coretypes.Kind, resourceID string, action coretypes.Verb) AuditEvent {
return AuditEvent{
Body: resourceKind.String() + "." + action.PastTense(),
EventName: NewEventName(resourceKind, action),
AuditAttributes: AuditAttributes{
Action: action,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
ResourceAttributes: ResourceAttributes{
ResourceKind: resourceKind,
ResourceID: resourceID,
},
}
}
func TestNewPLogsFromAuditEvents(t *testing.T) {
testCases := []struct {
name string
events []AuditEvent
expectedResourceLogs int
expectedResourceKinds []string
expectedResourceIDs []string
expectedLogRecordCounts []int
}{
{
name: "Empty",
events: []AuditEvent{},
expectedResourceLogs: 0,
},
{
name: "SingleEvent",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
},
expectedResourceLogs: 1,
expectedResourceKinds: []string{"dashboard"},
expectedResourceIDs: []string{"d-001"},
expectedLogRecordCounts: []int{1},
},
{
name: "SameResource_MultipleEvents",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(testDashboardKind, "d-001", coretypes.VerbDelete),
},
expectedResourceLogs: 1,
expectedResourceKinds: []string{"dashboard"},
expectedResourceIDs: []string{"d-001"},
expectedLogRecordCounts: []int{3},
},
{
name: "DifferentResources_SeparateGroups",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbDelete),
},
expectedResourceLogs: 2,
expectedResourceKinds: []string{"dashboard", "user"},
expectedResourceIDs: []string{"d-001", "u-001"},
expectedLogRecordCounts: []int{1, 1},
},
{
name: "SameKind_DifferentIDs_SeparateGroups",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(testDashboardKind, "d-002", coretypes.VerbDelete),
},
expectedResourceLogs: 2,
expectedResourceKinds: []string{"dashboard", "dashboard"},
expectedResourceIDs: []string{"d-001", "d-002"},
expectedLogRecordCounts: []int{1, 1},
},
{
name: "InterleavedResources_GroupedCorrectly",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbUpdate),
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbDelete),
newTestEvent(testDashboardKind, "d-001", coretypes.VerbDelete),
},
expectedResourceLogs: 2,
expectedResourceKinds: []string{"dashboard", "user"},
expectedResourceIDs: []string{"d-001", "u-001"},
expectedLogRecordCounts: []int{3, 2},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
logs := NewPLogsFromAuditEvents(testCase.events, "signoz", "0.90.0", "signoz.audit")
assert.Equal(t, testCase.expectedResourceLogs, logs.ResourceLogs().Len())
for i := 0; i < logs.ResourceLogs().Len(); i++ {
resourceLogs := logs.ResourceLogs().At(i)
resourceAttrs := resourceLogs.Resource().Attributes()
// Verify service resource attributes
serviceName, exists := resourceAttrs.Get("service.name")
assert.True(t, exists)
assert.Equal(t, "signoz", serviceName.Str())
serviceVersion, exists := resourceAttrs.Get("service.version")
assert.True(t, exists)
assert.Equal(t, "0.90.0", serviceVersion.Str())
// Verify audit resource attributes on Resource (not event attributes)
kind, exists := resourceAttrs.Get("signoz.audit.resource.kind")
assert.True(t, exists)
assert.Equal(t, testCase.expectedResourceKinds[i], kind.Str())
id, exists := resourceAttrs.Get("signoz.audit.resource.id")
assert.True(t, exists)
assert.Equal(t, testCase.expectedResourceIDs[i], id.Str())
// Verify scope
assert.Equal(t, 1, resourceLogs.ScopeLogs().Len())
assert.Equal(t, "signoz.audit", resourceLogs.ScopeLogs().At(0).Scope().Name())
// Verify log record count per group
assert.Equal(t, testCase.expectedLogRecordCounts[i], resourceLogs.ScopeLogs().At(0).LogRecords().Len())
// Verify resource attrs are NOT in log record event attributes
for j := 0; j < resourceLogs.ScopeLogs().At(0).LogRecords().Len(); j++ {
recordAttrs := resourceLogs.ScopeLogs().At(0).LogRecords().At(j).Attributes()
_, hasKind := recordAttrs.Get("signoz.audit.resource.kind")
_, hasID := recordAttrs.Get("signoz.audit.resource.id")
assert.False(t, hasKind, "resource.kind must not be in log record attributes")
assert.False(t, hasID, "resource.id must not be in log record attributes")
}
}
})
}
}

View File

@@ -38,6 +38,12 @@ var Kinds = []Kind{
KindMeterMetrics,
KindLogsField,
KindTracesField,
KindLLMPricingRule,
KindSpanMapperGroup,
KindSpanMapper,
KindZeusProfile,
KindZeusHost,
KindMetricField,
}
var (
@@ -78,4 +84,10 @@ var (
KindMeterMetrics = MustNewKind("meter-metrics")
KindLogsField = MustNewKind("logs-field")
KindTracesField = MustNewKind("traces-field")
KindLLMPricingRule = MustNewKind("llm-pricing-rule")
KindSpanMapperGroup = MustNewKind("span-mapper-group")
KindSpanMapper = MustNewKind("span-mapper")
KindZeusProfile = MustNewKind("zeus-profile")
KindZeusHost = MustNewKind("zeus-host")
KindMetricField = MustNewKind("metric-field")
)

View File

@@ -69,6 +69,18 @@ var Resources = []Resource{
ResourceMetaResourcesLogsField,
ResourceMetaResourceTracesField,
ResourceMetaResourcesTracesField,
ResourceMetaResourceLLMPricingRule,
ResourceMetaResourcesLLMPricingRule,
ResourceMetaResourceSpanMapperGroup,
ResourceMetaResourcesSpanMapperGroup,
ResourceMetaResourceSpanMapper,
ResourceMetaResourcesSpanMapper,
ResourceMetaResourceZeusProfile,
ResourceMetaResourcesZeusProfile,
ResourceMetaResourceZeusHost,
ResourceMetaResourcesZeusHost,
ResourceMetaResourceMetricField,
ResourceMetaResourcesMetricField,
}
var (
@@ -140,4 +152,16 @@ var (
ResourceMetaResourcesLogsField = NewResourceMetaResources(KindLogsField)
ResourceMetaResourceTracesField = NewResourceMetaResource(KindTracesField)
ResourceMetaResourcesTracesField = NewResourceMetaResources(KindTracesField)
ResourceMetaResourceLLMPricingRule = NewResourceMetaResource(KindLLMPricingRule)
ResourceMetaResourcesLLMPricingRule = NewResourceMetaResources(KindLLMPricingRule)
ResourceMetaResourceSpanMapperGroup = NewResourceMetaResource(KindSpanMapperGroup)
ResourceMetaResourcesSpanMapperGroup = NewResourceMetaResources(KindSpanMapperGroup)
ResourceMetaResourceSpanMapper = NewResourceMetaResource(KindSpanMapper)
ResourceMetaResourcesSpanMapper = NewResourceMetaResources(KindSpanMapper)
ResourceMetaResourceZeusProfile = NewResourceMetaResource(KindZeusProfile)
ResourceMetaResourcesZeusProfile = NewResourceMetaResources(KindZeusProfile)
ResourceMetaResourceZeusHost = NewResourceMetaResource(KindZeusHost)
ResourceMetaResourcesZeusHost = NewResourceMetaResources(KindZeusHost)
ResourceMetaResourceMetricField = NewResourceMetaResource(KindMetricField)
ResourceMetaResourcesMetricField = NewResourceMetaResources(KindMetricField)
)

View File

@@ -9,7 +9,6 @@ type TTLSetting struct {
bun.BaseModel `bun:"table:ttl_setting"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
TransactionID string `bun:"transaction_id,type:text,notnull"`
TableName string `bun:"table_name,type:text,notnull"`
TTL int `bun:"ttl,notnull,default:0"`

View File

@@ -13,6 +13,7 @@ pytest_plugins = [
"fixtures.zookeeper",
"fixtures.signoz",
"fixtures.audit",
"fixtures.auditor",
"fixtures.logs",
"fixtures.traces",
"fixtures.metrics",

183
tests/fixtures/auditor.py vendored Normal file
View File

@@ -0,0 +1,183 @@
import json
import os
import time
from collections.abc import Callable
from dataclasses import dataclass
from http import HTTPStatus
from typing import Any
import pytest
import requests
from fixtures import reuse, types
from fixtures.auth import find_user_by_email
# Filename used for the audit log inside the host-mounted tmp dir. The same
# absolute path is mounted into the SigNoz container at the same location, so
# the file is visible to both the backend (writer) and the test runner (reader).
AUDIT_FILE_NAME = "audit.log"
@dataclass
class _AuditDir:
"""Cacheable wrapper around the audit directory path so the reuse.wrap
machinery can persist it across pytest runs alongside the SigNoz container."""
path: str
def __cache__(self) -> dict:
return {"path": self.path}
def __log__(self) -> str:
return f"AuditDir(path={self.path})"
@pytest.fixture(name="audit_dir", scope="package")
def audit_dir(
tmpfs: Callable[[str], types.LegacyPath],
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> str:
"""Host tmp directory mounted into the SigNoz container as the auditor file path.
Mirrors the sqlite/clickhouse pattern: a tmpfs directory is created on the
host and bind-mounted into the container at the same absolute path, so the
audit log file is reachable from both sides without docker exec. The path
is cached via reuse.wrap so re-runs under --reuse keep using the same dir
that the long-lived SigNoz container has bind-mounted.
"""
def create() -> _AuditDir:
return _AuditDir(path=str(tmpfs("auditor")))
def delete(_: _AuditDir) -> None:
pass
def restore(cache: dict) -> _AuditDir:
return _AuditDir(path=cache["path"])
return reuse.wrap(
request,
pytestconfig,
"auditor_dir",
lambda: _AuditDir(path=""),
create,
delete,
restore,
).path
@pytest.fixture(name="audit_file_path", scope="package")
def audit_file_path(audit_dir: str) -> str: # pylint: disable=redefined-outer-name
return os.path.join(audit_dir, AUDIT_FILE_NAME)
def ensure_user_active(
signoz: types.SigNoz,
admin_token: str,
email: str,
role: str,
password: str,
name: str = "",
) -> str:
"""Invite + activate a user, or return the existing user's id if already present.
Idempotent counterpart to fixtures.auth.create_active_user — needed because the
auditor suite reuses a long-lived SigNoz container across pytest runs and would
otherwise hit a 409 on the second invite.
"""
invite = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite"),
json={"email": email, "role": role, "name": name},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
if invite.status_code == HTTPStatus.CONFLICT:
return find_user_by_email(signoz, admin_token, email)["id"]
assert invite.status_code == HTTPStatus.CREATED, invite.text
invited_user = invite.json()["data"]
activate = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
json={"password": password, "token": invited_user["token"]},
timeout=5,
)
assert activate.status_code == HTTPStatus.NO_CONTENT, activate.text
return invited_user["id"]
def read_audit_records(audit_file_path: str) -> list[dict[str, Any]]: # pylint: disable=redefined-outer-name
"""Read every audit log record from the host-side audit file.
Each line of the file is one OTLP-Logs JSON object containing all events
flushed in a single export batch. Returns the flattened list of LogRecord
dicts across every line, with the parent resource attributes merged into
each record's attributes so signoz.audit.resource.kind and
signoz.audit.resource.id are reachable via attr_value.
"""
if not os.path.exists(audit_file_path):
return []
records: list[dict[str, Any]] = []
with open(audit_file_path, encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
payload = json.loads(line)
except json.JSONDecodeError:
# Partial line caught mid-write between flush syscalls; the
# next poll will re-read the full file once the write
# completes. Skip without failing the test.
continue
for resource_log in payload.get("resourceLogs", []):
resource_attrs = resource_log.get("resource", {}).get("attributes", [])
for scope_log in resource_log.get("scopeLogs", []):
for record in scope_log.get("logRecords", []):
merged = dict(record)
merged["attributes"] = list(record.get("attributes", [])) + list(resource_attrs)
records.append(merged)
return records
def attr_value(record: dict[str, Any], key: str) -> Any:
"""Return the value of an OTLP-JSON attribute by key, or None if absent."""
for kv in record.get("attributes", []):
if kv.get("key") != key:
continue
value = kv.get("value", {})
for kind in ("stringValue", "intValue", "boolValue", "doubleValue"):
if kind in value:
return value[kind]
return None
return None
def find_event(records: list[dict[str, Any]], event_name: str, **filters: Any) -> dict[str, Any] | None:
"""Find the first record whose eventName matches and whose audit attributes match every filter."""
for record in records:
if record.get("eventName") != event_name:
continue
if all(attr_value(record, k) == v for k, v in filters.items()):
return record
return None
def wait_for_event(
audit_file_path: str, # pylint: disable=redefined-outer-name
event_name: str,
timeout: float = 2.0,
interval: float = 0.1,
**filters: Any,
) -> dict[str, Any]:
"""Poll the audit file until an event matching event_name + filters appears."""
deadline = time.monotonic() + timeout
last_records: list[dict[str, Any]] = []
while time.monotonic() < deadline:
last_records = read_audit_records(audit_file_path)
event = find_event(last_records, event_name, **filters)
if event is not None:
return event
time.sleep(interval)
raise AssertionError(f"audit event {event_name!r} matching {filters} not found within {timeout}s (saw {len(last_records)} records: {[r.get('eventName') for r in last_records]})")

View File

@@ -16,7 +16,7 @@ from fixtures.logger import setup_logger
logger = setup_logger(__name__)
def create_signoz(
def create_signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
network: Network,
zeus: types.TestContainerDocker,
gateway: types.TestContainerDocker,
@@ -26,10 +26,15 @@ def create_signoz(
pytestconfig: pytest.Config,
cache_key: str = "signoz",
env_overrides: dict | None = None,
volume_mappings: list[tuple[str, str]] | None = None,
) -> types.SigNoz:
"""
Factory function for creating a SigNoz container.
Accepts optional env_overrides to customize the container environment.
Accepts optional env_overrides to customize the container environment, and
optional volume_mappings (host_path, container_path) tuples to mount host
directories into the container — mirrors how sqlite/clickhouse fixtures
expose tmp paths back to the test runner.
"""
def create() -> types.SigNoz:
@@ -104,6 +109,10 @@ def create_signoz(
"rw",
)
if volume_mappings:
for host_path, container_path in volume_mappings:
container.with_volume_mapping(host_path, container_path, "rw")
container.start()
def ready(container: DockerContainer) -> None:

View File

@@ -0,0 +1,91 @@
from collections.abc import Callable
from http import HTTPStatus
import requests
from fixtures import types
from fixtures.auditor import attr_value, ensure_user_active, wait_for_event
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
USER_VIEWER_EMAIL,
USER_VIEWER_NAME,
USER_VIEWER_PASSWORD,
find_user_by_email,
)
from fixtures.logger import setup_logger
logger = setup_logger(__name__)
def test_session_deleted_event_appears_in_file(
signoz: types.SigNoz,
apply_license: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
audit_file_path: str,
) -> None:
"""An admin logout posts to DELETE /api/v2/sessions; the audit middleware
captures the post-auth claims and the file provider writes session.deleted."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.delete(
signoz.self.host_configs["8080"].get("/api/v2/sessions"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
record = wait_for_event(
audit_file_path,
"session.deleted",
**{
"signoz.audit.outcome": "success",
"signoz.audit.verb": "delete",
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
},
)
assert attr_value(record, "signoz.audit.resource.kind") == "session"
assert attr_value(record, "signoz.audit.action_category") == "access_control"
assert record["severityText"] == "INFO"
def test_audit_records_failure_outcome(
signoz: types.SigNoz,
apply_license: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
audit_file_path: str,
) -> None:
"""A viewer hitting an admin-only mutation must produce an audit record with
outcome=failure and the captured error.type. Proves the middleware writes
on the 4xx path, not just the happy path."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
ensure_user_active(
signoz,
admin_token,
USER_VIEWER_EMAIL,
"VIEWER",
USER_VIEWER_PASSWORD,
USER_VIEWER_NAME,
)
admin_user = find_user_by_email(signoz, admin_token, USER_ADMIN_EMAIL)
viewer_token = get_token(USER_VIEWER_EMAIL, USER_VIEWER_PASSWORD)
forbidden = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{admin_user['id']}"),
json={"displayName": "should not work"},
headers={"Authorization": f"Bearer {viewer_token}"},
timeout=5,
)
assert forbidden.status_code == HTTPStatus.FORBIDDEN, forbidden.text
record = wait_for_event(
audit_file_path,
"user.updated",
**{
"signoz.audit.outcome": "failure",
"signoz.audit.principal.email": USER_VIEWER_EMAIL,
"signoz.audit.resource.id": admin_user["id"],
},
)
assert record["severityText"] == "ERROR"
assert attr_value(record, "signoz.audit.error.type") is not None

View File

@@ -0,0 +1,122 @@
from collections.abc import Callable
from http import HTTPStatus
import requests
from fixtures import types
from fixtures.auditor import attr_value, ensure_user_active, wait_for_event
from fixtures.auth import (
USER_ADMIN_EMAIL,
USER_ADMIN_PASSWORD,
USER_EDITOR_EMAIL,
USER_EDITOR_NAME,
USER_EDITOR_PASSWORD,
change_user_role,
find_user_by_email,
)
from fixtures.logger import setup_logger
logger = setup_logger(__name__)
def test_user_updated_event_appears_in_file(
signoz: types.SigNoz,
apply_license: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
audit_file_path: str,
) -> None:
"""Admin renames an editor user via PUT /api/v2/users/{id}; the file provider
writes user.updated with the editor id as the resource."""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
editor_id = ensure_user_active(
signoz,
admin_token,
USER_EDITOR_EMAIL,
"EDITOR",
USER_EDITOR_PASSWORD,
USER_EDITOR_NAME,
)
response = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{editor_id}"),
json={"displayName": "Renamed Editor"},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
record = wait_for_event(
audit_file_path,
"user.updated",
**{
"signoz.audit.outcome": "success",
"signoz.audit.verb": "update",
"signoz.audit.resource.id": editor_id,
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
},
)
assert attr_value(record, "signoz.audit.action_category") == "configuration_change"
assert attr_value(record, "signoz.audit.resource.kind") == "user"
def test_user_role_change_emits_created_and_deleted_events(
signoz: types.SigNoz,
apply_license: types.Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
audit_file_path: str,
) -> None:
"""Toggling the editor user's managed role between signoz-editor and signoz-viewer
fires both DELETE and POST against /api/v2/users/{id}/roles; the file provider
writes one user-role.deleted and one user-role.created tied to the editor id.
The toggle direction is computed from the current role so the test is idempotent
across re-runs of the long-lived auditor SigNoz container.
"""
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
editor = find_user_by_email(signoz, admin_token, USER_EDITOR_EMAIL)
editor_id = editor["id"]
roles_response = requests.get(
signoz.self.host_configs["8080"].get(f"/api/v2/users/{editor_id}/roles"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert roles_response.status_code == HTTPStatus.OK, roles_response.text
managed_role = next(
(r for r in roles_response.json()["data"] if r["name"] in ("signoz-editor", "signoz-viewer")),
None,
)
assert managed_role is not None, "editor user is missing both managed roles"
if managed_role["name"] == "signoz-editor":
old_role, new_role = "signoz-editor", "signoz-viewer"
else:
old_role, new_role = "signoz-viewer", "signoz-editor"
change_user_role(signoz, admin_token, editor_id, old_role, new_role)
deleted = wait_for_event(
audit_file_path,
"user-role.deleted",
**{
"signoz.audit.outcome": "success",
"signoz.audit.verb": "delete",
"signoz.audit.resource.id": editor_id,
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
},
)
assert attr_value(deleted, "signoz.audit.action_category") == "access_control"
assert attr_value(deleted, "signoz.audit.resource.kind") == "user-role"
created = wait_for_event(
audit_file_path,
"user-role.created",
**{
"signoz.audit.outcome": "success",
"signoz.audit.verb": "create",
"signoz.audit.resource.id": editor_id,
"signoz.audit.principal.email": USER_ADMIN_EMAIL,
},
)
assert attr_value(created, "signoz.audit.action_category") == "access_control"
assert attr_value(created, "signoz.audit.resource.kind") == "user-role"

View File

@@ -0,0 +1,44 @@
import pytest
from testcontainers.core.container import Network
from fixtures import types
from fixtures.signoz import create_signoz
@pytest.fixture(name="signoz", scope="package")
def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
network: Network,
zeus: types.TestContainerDocker,
gateway: types.TestContainerDocker,
sqlstore: types.TestContainerSQL,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
audit_dir: str,
audit_file_path: str,
) -> types.SigNoz:
"""Package-scoped SigNoz container configured with the file auditor.
BatchSize is set to 1 so every audited request flushes to disk on the moreC
path without waiting on the periodic ticker. FlushInterval stays short so
the periodic flush has bounded lag if BatchSize is ever raised. The audit
directory is bind-mounted from the host (see fixtures.auditor.audit_dir)
so tests can read the file with a plain open() call.
"""
return create_signoz(
network=network,
zeus=zeus,
gateway=gateway,
sqlstore=sqlstore,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
cache_key="signoz_auditor",
env_overrides={
"SIGNOZ_AUDITOR_PROVIDER": "file",
"SIGNOZ_AUDITOR_FILE_PATH": audit_file_path,
"SIGNOZ_AUDITOR_BATCH__SIZE": "1",
"SIGNOZ_AUDITOR_FLUSH__INTERVAL": "100ms",
},
volume_mappings=[(audit_dir, audit_dir)],
)

View File

@@ -37,7 +37,7 @@ def test_audit_list_all(
"signoz.audit.principal.id": "user-010",
"signoz.audit.principal.email": "ops@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "create",
"signoz.audit.verb": "create",
"signoz.audit.outcome": "success",
},
body="ops@acme.com (user-010) created alert-rule (alert-001)",
@@ -55,7 +55,7 @@ def test_audit_list_all(
"signoz.audit.principal.id": "user-010",
"signoz.audit.principal.email": "ops@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "update",
"signoz.audit.verb": "update",
"signoz.audit.outcome": "success",
},
body="ops@acme.com (user-010) updated saved-view (view-001)",
@@ -73,7 +73,7 @@ def test_audit_list_all(
"signoz.audit.principal.id": "user-010",
"signoz.audit.principal.email": "ops@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "update",
"signoz.audit.verb": "update",
"signoz.audit.action_category": "access_control",
"signoz.audit.outcome": "success",
},
@@ -146,7 +146,7 @@ def test_audit_list_all(
id="filter_by_principal_type",
),
pytest.param(
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
1,
{"dashboard.deleted"},
id="filter_by_resource_kind_and_action",
@@ -177,7 +177,7 @@ def test_audit_filter(
"signoz.audit.principal.id": "user-001",
"signoz.audit.principal.email": "alice@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "create",
"signoz.audit.verb": "create",
"signoz.audit.action_category": "configuration_change",
"signoz.audit.outcome": "success",
},
@@ -195,7 +195,7 @@ def test_audit_filter(
"signoz.audit.principal.id": "user-001",
"signoz.audit.principal.email": "alice@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "update",
"signoz.audit.verb": "update",
"signoz.audit.action_category": "configuration_change",
"signoz.audit.outcome": "success",
},
@@ -213,7 +213,7 @@ def test_audit_filter(
"signoz.audit.principal.id": "user-002",
"signoz.audit.principal.email": "viewer@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "delete",
"signoz.audit.verb": "delete",
"signoz.audit.action_category": "configuration_change",
"signoz.audit.outcome": "failure",
"signoz.audit.error.type": "forbidden",
@@ -234,7 +234,7 @@ def test_audit_filter(
"signoz.audit.principal.id": "sa-001",
"signoz.audit.principal.email": "",
"signoz.audit.principal.type": "service_account",
"signoz.audit.action": "create",
"signoz.audit.verb": "create",
"signoz.audit.action_category": "access_control",
"signoz.audit.outcome": "success",
},
@@ -252,7 +252,7 @@ def test_audit_filter(
"signoz.audit.principal.id": "user-001",
"signoz.audit.principal.email": "alice@acme.com",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "login",
"signoz.audit.verb": "login",
"signoz.audit.action_category": "access_control",
"signoz.audit.outcome": "success",
},
@@ -310,7 +310,7 @@ def test_audit_scalar_count_failures(
attributes={
"signoz.audit.principal.id": "user-050",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "delete",
"signoz.audit.verb": "delete",
"signoz.audit.outcome": "failure",
},
body="user-050 failed to delete dashboard",
@@ -327,7 +327,7 @@ def test_audit_scalar_count_failures(
attributes={
"signoz.audit.principal.id": "user-060",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "update",
"signoz.audit.verb": "update",
"signoz.audit.outcome": "failure",
},
body="user-060 failed to update alert-rule",
@@ -344,7 +344,7 @@ def test_audit_scalar_count_failures(
attributes={
"signoz.audit.principal.id": "user-050",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "update",
"signoz.audit.verb": "update",
"signoz.audit.outcome": "success",
},
body="user-050 updated dashboard",
@@ -400,7 +400,7 @@ def test_audit_does_not_leak_into_logs(
attributes={
"signoz.audit.principal.id": "user-admin",
"signoz.audit.principal.type": "user",
"signoz.audit.action": "update",
"signoz.audit.verb": "update",
"signoz.audit.outcome": "success",
},
body="user-admin updated organization (org-999)",
@@ -430,5 +430,5 @@ def test_audit_does_not_leak_into_logs(
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.action", "")]
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.verb", "")]
assert len(audit_bodies) == 0