Compare commits

...

2 Commits

Author SHA1 Message Date
vikrantgupta25
49e9657cae feat(resource): add related resources 2026-06-08 00:17:06 +05:30
vikrantgupta25
d296ce0f3f feat(resource): initial commit 2026-06-06 17:28:50 +05:30
18 changed files with 1260 additions and 561 deletions

View File

@@ -185,6 +185,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewResource(s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewComment().Wrap)

View File

@@ -55,6 +55,11 @@ func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef {
return nil
}
func (handler *healthOpenAPIHandler) ResourceDefs() []pkghandler.ResourceSpec {
// Health endpoints don't act on resources.
return nil
}
func (provider *provider) addRegistryRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/healthz", newHealthOpenAPIHandler(
provider.authzMiddleware.OpenAccess(provider.factoryHandler.Healthz),

View File

@@ -7,166 +7,190 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (provider *provider) addRoleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Create, authtypes.Relation{Verb: coretypes.VerbCreate}, coretypes.ResourceRole, roleCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), 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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/roles", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Create, authtypes.SigNozAdminRoleName),
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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbCreate,
ID: handler.ResponseJSONPath("data.id"),
Selector: handler.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles", handler.New(provider.authzMiddleware.Check(provider.authzHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceRole, roleCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListRoles",
Tags: []string{"role"},
Summary: "List roles",
Description: "This endpoint lists all roles",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/roles", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.List, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ListRoles",
Tags: []string{"role"},
Summary: "List roles",
Description: "This endpoint lists all roles",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbList,
Selector: handler.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Get, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetRole",
Tags: []string{"role"},
Summary: "Get role",
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(authtypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Get, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetRole",
Tags: []string{"role"},
Summary: "Get role",
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(authtypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbRead,
ID: handler.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.Check(provider.authzHandler.GetObjects, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetObjects",
Tags: []string{"role"},
Summary: "Get objects for a role by relation",
Description: "Gets all objects connected to the specified role via a given relation type",
Request: nil,
RequestContentType: "",
Response: make([]*coretypes.ObjectGroup, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.GetObjects, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetObjects",
Tags: []string{"role"},
Summary: "Get objects for a role by relation",
Description: "Gets all objects connected to the specified role via a given relation type",
Request: nil,
RequestContentType: "",
Response: make([]*coretypes.ObjectGroup, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound, http.StatusNotImplemented, http.StatusUnavailableForLegalReasons},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbRead,
ID: handler.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Patch, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), 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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Patch, authtypes.SigNozAdminRoleName),
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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbUpdate,
ID: handler.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(provider.authzMiddleware.Check(provider.authzHandler.PatchObjects, authtypes.Relation{Verb: coretypes.VerbUpdate}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), 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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPatch).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}/relations/{relation}/objects", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.PatchObjects, authtypes.SigNozAdminRoleName),
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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbUpdate,
ID: handler.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authzMiddleware.Check(provider.authzHandler.Delete, authtypes.Relation{Verb: coretypes.VerbDelete}, coretypes.ResourceRole, provider.roleInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), 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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbDelete)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/roles/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.authzHandler.Delete, authtypes.SigNozAdminRoleName),
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: newScopedSecuritySchemes([]string{coretypes.ResourceRole.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbDelete,
ID: handler.PathParam("id"),
Selector: provider.roleSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}
func roleCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func (provider *provider) roleInstanceSelectorCallback(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
roleID, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
return nil, err
}
role, err := provider.authzService.Get(req.Context(), valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
return nil, err
}
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(role.Name),
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}

View File

@@ -1,13 +1,10 @@
package signozapiserver
import (
"bytes"
"encoding/json"
"io"
"context"
"net/http"
"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/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
@@ -17,41 +14,54 @@ 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.ResourceServiceAccount, 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.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Create, 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.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbCreate,
ID: handler.ResponseJSONPath("data.id"),
Selector: handler.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.List, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceServiceAccount, serviceAccountCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListServiceAccounts",
Tags: []string{"serviceaccount"},
Summary: "List service accounts",
Description: "This endpoint lists the service accounts for an organisation",
Request: nil,
RequestContentType: "",
Response: make([]*serviceaccounttypes.ServiceAccount, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.List, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ListServiceAccounts",
Tags: []string{"serviceaccount"},
Summary: "List service accounts",
Description: "This endpoint lists the service accounts for an organisation",
Request: nil,
RequestContentType: "",
Response: make([]*serviceaccounttypes.ServiceAccount, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbList,
Selector: handler.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
@@ -72,89 +82,125 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.Get, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Gets a service account",
Description: "This endpoint gets an existing service account",
Request: nil,
RequestContentType: "",
Response: new(serviceaccounttypes.ServiceAccountWithRoles),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Get, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetServiceAccount",
Tags: []string{"serviceaccount"},
Summary: "Gets a service account",
Description: "This endpoint gets an existing service account",
Request: nil,
RequestContentType: "",
Response: new(serviceaccounttypes.ServiceAccountWithRoles),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbRead,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.GetRoles, authtypes.Relation{Verb: coretypes.VerbRead}, coretypes.ResourceServiceAccount, serviceAccountInstanceSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "GetServiceAccountRoles",
Tags: []string{"serviceaccount"},
Summary: "Gets service account roles",
Description: "This endpoint gets all the roles for the existing service account",
Request: nil,
RequestContentType: "",
Response: new([]*authtypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.GetRoles, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetServiceAccountRoles",
Tags: []string{"serviceaccount"},
Summary: "Gets service account roles",
Description: "This endpoint gets all the roles for the existing service account",
Request: nil,
RequestContentType: "",
Response: new([]*authtypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbRead,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
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{
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)}),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/roles", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.SetRole, 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.WithResourceDefs(
handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbAttach,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
Related: &handler.RelatedResource{Resource: coretypes.ResourceRole, ID: handler.BodyJSONPath("id")},
},
handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbAttach,
ID: handler.BodyJSONPath("id"),
Selector: provider.roleSelector,
},
),
)).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.VerbDetach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceRole, SelectorCallback: provider.roleDetachSelectorFromPath, 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.VerbDetach), coretypes.ResourceRole.Scope(coretypes.VerbDetach)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/roles/{rid}", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.DeleteRole, 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.VerbDetach), coretypes.ResourceRole.Scope(coretypes.VerbDetach)}),
},
handler.WithResourceDefs(
handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbDetach,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
Related: &handler.RelatedResource{Resource: coretypes.ResourceRole, ID: handler.PathParam("rid")},
},
handler.ResourceDef{
Resource: coretypes.ResourceRole,
Verb: coretypes.VerbDetach,
ID: handler.PathParam("rid"),
Selector: provider.roleSelector,
},
),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
@@ -175,208 +221,199 @@ func (provider *provider) addServiceAccountRoutes(router *mux.Router) error {
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 {
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Update, 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)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbUpdate,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
}),
)).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 {
if err := router.Handle("/api/v1/service_accounts/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Delete, 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)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbDelete,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.CreateFactorAPIKey, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbCreate}, Resource: coretypes.ResourceMetaResourceFactorAPIKey, SelectorCallback: factorAPIKeyCollectionSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbAttach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []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.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbCreate), coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach)}),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.CreateFactorAPIKey, 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.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbCreate), coretypes.ResourceServiceAccount.Scope(coretypes.VerbAttach)}),
},
handler.WithResourceDefs(
handler.ResourceDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbCreate,
ID: handler.ResponseJSONPath("data.id"),
Selector: handler.WildcardSelector,
},
handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbAttach,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
Related: &handler.RelatedResource{Resource: coretypes.ResourceMetaResourceFactorAPIKey, ID: handler.ResponseJSONPath("data.id")},
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(provider.authzMiddleware.Check(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.Relation{Verb: coretypes.VerbList}, coretypes.ResourceMetaResourceFactorAPIKey, factorAPIKeyCollectionSelectorCallback, []string{
authtypes.SigNozAdminRoleName,
}), handler.OpenAPIDef{
ID: "ListServiceAccountKeys",
Tags: []string{"serviceaccount"},
Summary: "List service account keys",
Description: "This endpoint lists the service account keys",
Request: nil,
RequestContentType: "",
Response: make([]*serviceaccounttypes.GettableFactorAPIKey, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbList)}),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/keys", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.ListFactorAPIKey, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ListServiceAccountKeys",
Tags: []string{"serviceaccount"},
Summary: "List service account keys",
Description: "This endpoint lists the service account keys",
Request: nil,
RequestContentType: "",
Response: make([]*serviceaccounttypes.GettableFactorAPIKey, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbList,
Selector: handler.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
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.ResourceMetaResourceFactorAPIKey, factorAPIKeyInstanceSelectorCallback, []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.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbUpdate)}),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.UpdateFactorAPIKey, 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.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.ResourceDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbUpdate,
ID: handler.PathParam("fid"),
Selector: handler.IDSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(provider.authzMiddleware.CheckAll(provider.serviceAccountHandler.RevokeFactorAPIKey, []middleware.AuthZCheckGroup{
{{Relation: authtypes.Relation{Verb: coretypes.VerbDelete}, Resource: coretypes.ResourceMetaResourceFactorAPIKey, SelectorCallback: factorAPIKeyInstanceSelectorCallback, Roles: []string{
authtypes.SigNozAdminRoleName,
}}},
{{Relation: authtypes.Relation{Verb: coretypes.VerbDetach}, Resource: coretypes.ResourceServiceAccount, SelectorCallback: serviceAccountInstanceSelectorCallback, Roles: []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.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbDelete), coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach)}),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/service_accounts/{id}/keys/{fid}", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.RevokeFactorAPIKey, 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.ResourceMetaResourceFactorAPIKey.Scope(coretypes.VerbDelete), coretypes.ResourceServiceAccount.Scope(coretypes.VerbDetach)}),
},
handler.WithResourceDefs(
handler.ResourceDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey,
Verb: coretypes.VerbDelete,
ID: handler.PathParam("fid"),
Selector: handler.IDSelector,
},
handler.ResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbDetach,
ID: handler.PathParam("id"),
Selector: handler.IDSelector,
Related: &handler.RelatedResource{Resource: coretypes.ResourceMetaResourceFactorAPIKey, ID: handler.PathParam("fid")},
},
),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}
func (provider *provider) roleDetachSelectorFromPath(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
roleID, err := valuer.NewUUID(mux.Vars(req)["rid"])
// roleSelector resolves the FGA selectors for a role from its UUID. The id is
// already extracted by the ResourceDef (path or body); this only does the
// UUID -> name lookup the FGA object string requires. Shared by service account
// and role routes.
func (provider *provider) roleSelector(ctx context.Context, resource coretypes.Resource, id string, claims authtypes.Claims) ([]coretypes.Selector, error) {
roleID, err := valuer.NewUUID(id)
if err != nil {
return nil, err
}
role, err := provider.authzService.Get(req.Context(), valuer.MustNewUUID(claims.OrgID), roleID)
role, err := provider.authzService.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
return nil, err
}
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(role.Name),
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func (provider *provider) roleAttachSelectorFromBody(req *http.Request, claims authtypes.Claims) ([]coretypes.Selector, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
postableRole := new(serviceaccounttypes.PostableServiceAccountRole)
if err := json.Unmarshal(body, postableRole); err != nil {
return nil, err
}
role, err := provider.authzService.Get(req.Context(), valuer.MustNewUUID(claims.OrgID), postableRole.ID)
if err != nil {
return nil, err
}
return []coretypes.Selector{
coretypes.TypeRole.MustSelector(role.Name),
coretypes.TypeRole.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func factorAPIKeyCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func factorAPIKeyInstanceSelectorCallback(req *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
fid := mux.Vars(req)["fid"]
fidSelector, err := coretypes.TypeMetaResource.Selector(fid)
if err != nil {
return nil, err
}
return []coretypes.Selector{
fidSelector,
coretypes.TypeMetaResource.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func serviceAccountCollectionSelectorCallback(_ *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{
coretypes.TypeServiceAccount.MustSelector(coretypes.WildCardSelectorString),
}, nil
}
func serviceAccountInstanceSelectorCallback(req *http.Request, _ authtypes.Claims) ([]coretypes.Selector, error) {
id := mux.Vars(req)["id"]
idSelector, err := coretypes.TypeServiceAccount.Selector(id)
if err != nil {
return nil, err
}
return []coretypes.Selector{
idSelector,
coretypes.TypeServiceAccount.MustSelector(coretypes.WildCardSelectorString),
resource.Type().MustSelector(role.Name),
resource.Type().MustSelector(coretypes.WildCardSelectorString),
}, nil
}

View File

@@ -16,12 +16,14 @@ type Handler interface {
http.Handler
ServeOpenAPI(openapi.OperationContext)
AuditDef() *AuditDef
ResourceDefs() []ResourceSpec
}
type handler struct {
handlerFunc http.HandlerFunc
openAPIDef OpenAPIDef
auditDef *AuditDef
handlerFunc http.HandlerFunc
openAPIDef OpenAPIDef
auditDef *AuditDef
resourceDefs []ResourceSpec
}
func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Handler {
@@ -47,6 +49,14 @@ func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Ha
opt(handler)
}
// Resource defs are a developer-authored, registration-time declaration —
// fail fast and loud on misconfiguration, like coretypes' MustSelector.
for _, def := range handler.resourceDefs {
if err := def.validate(); err != nil {
panic(err)
}
}
return handler
}
@@ -133,3 +143,7 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) {
func (handler *handler) AuditDef() *AuditDef {
return handler.auditDef
}
func (handler *handler) ResourceDefs() []ResourceSpec {
return handler.resourceDefs
}

View File

@@ -23,3 +23,12 @@ func WithAuditDef(def AuditDef) Option {
h.auditDef = &def
}
}
// WithResourceDefs attaches one or more resource specs (ResourceDef /
// ResourcesDef) to the handler. The resource middleware resolves them and the
// authz + audit middlewares read the result.
func WithResourceDefs(defs ...ResourceSpec) Option {
return func(h *handler) {
h.resourceDefs = append(h.resourceDefs, defs...)
}
}

View File

@@ -0,0 +1,299 @@
package handler
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
"github.com/tidwall/gjson"
)
var errCodeInvalidResourceDef = errors.MustNewCode("invalid_resource_def")
// ExtractorContext carries everything an extractor may read. The resource
// middleware fills Request + RequestBody pre-handler; the audit middleware adds
// ResponseBody post-handler. Each extractor is run exactly once, in the phase
// whose data it needs.
type ExtractorContext struct {
Request *http.Request
RequestBody []byte
ResponseBody []byte
}
// extractPhase marks whether an extractor reads request-side data (resolved
// pre-handler by the resource middleware) or response-side data (resolved
// post-handler by the audit middleware).
type extractPhase int
const (
phaseRequest extractPhase = iota
phaseResponse
)
// ResourceIDExtractor resolves a single resource id. Phase-tagged so the
// resolver runs it exactly once in the right phase. The declaration API exposes
// only the constructors below, so the phase is an internal detail.
type ResourceIDExtractor struct {
phase extractPhase
fn func(ExtractorContext) (string, error)
}
// ResourceIDsExtractor resolves multiple resource ids (fan-out). Always
// request-phase — arrays come from the request body.
type ResourceIDsExtractor struct {
phase extractPhase
fn func(ExtractorContext) ([]string, error)
}
// PathParam reads a gorilla/mux path variable. Request-phase.
func PathParam(name string) ResourceIDExtractor {
return ResourceIDExtractor{phase: phaseRequest, fn: func(ec ExtractorContext) (string, error) {
if ec.Request == nil {
return "", nil
}
return mux.Vars(ec.Request)[name], nil
}}
}
// BodyJSONPath reads a gjson path from the request body. Request-phase.
func BodyJSONPath(path string) ResourceIDExtractor {
return ResourceIDExtractor{phase: phaseRequest, fn: func(ec ExtractorContext) (string, error) {
return gjson.GetBytes(ec.RequestBody, path).String(), nil
}}
}
// BodyJSONArray reads a JSON array of strings from the request body. Request-phase.
func BodyJSONArray(path string) ResourceIDsExtractor {
return ResourceIDsExtractor{phase: phaseRequest, fn: func(ec ExtractorContext) ([]string, error) {
result := gjson.GetBytes(ec.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
}}
}
// ResponseJSONPath reads a gjson path from the response body. Response-phase —
// yields "" pre-handler and the real value post-handler.
func ResponseJSONPath(path string) ResourceIDExtractor {
return ResourceIDExtractor{phase: phaseResponse, fn: func(ec ExtractorContext) (string, error) {
return gjson.GetBytes(ec.ResponseBody, path).String(), nil
}}
}
// SelectorFunc maps a resolved id (+ its resource) to authz FGA selectors. It is
// the sole source of selectors — there is no default fallback to wildcard. Given
// a missing id it decides for itself whether to return a wildcard or an error. It
// never reads the request/body; ctx + claims are only for an optional DB lookup
// (e.g. role UUID -> name).
type SelectorFunc func(ctx context.Context, resource coretypes.Resource, id string, claims authtypes.Claims) ([]coretypes.Selector, error)
// WildcardSelector ignores the id and returns the resource's wildcard selector.
// Use for create / list / collection routes.
var WildcardSelector SelectorFunc = func(_ context.Context, resource coretypes.Resource, _ string, _ authtypes.Claims) ([]coretypes.Selector, error) {
return []coretypes.Selector{resource.Type().MustSelector(coretypes.WildCardSelectorString)}, nil
}
// IDSelector returns [exact, wildcard] for a present id and errors when the id is
// missing. Use for instance routes whose id is in the path/body.
var IDSelector SelectorFunc = func(_ context.Context, resource coretypes.Resource, id string, _ authtypes.Claims) ([]coretypes.Selector, error) {
if id == "" {
return nil, errors.Newf(errors.TypeInvalidInput, errCodeInvalidResourceDef, "resource id is required for %s", resource.Kind().String())
}
selector, err := resource.Type().Selector(id)
if err != nil {
return nil, err
}
return []coretypes.Selector{selector, resource.Type().MustSelector(coretypes.WildCardSelectorString)}, nil
}
// ResourceSpec is the sealed interface implemented by ResourceDef and
// ResourcesDef. Only these two satisfy WithResourceDefs.
type ResourceSpec interface {
sealResourceSpec()
validate() error
resolveRequest(ec ExtractorContext) []ResolvedResource
}
// ResourceDef declares one resource an operation acts on. For attach/detach,
// Related names the counterpart for audit clarity only — it is never authz-checked.
type ResourceDef struct {
Resource coretypes.Resource
Verb coretypes.Verb
ID ResourceIDExtractor
Selector SelectorFunc
Related *RelatedResource
}
// ResourcesDef declares many resources of one kind (fan-out). One resolved
// entry is produced per id.
type ResourcesDef struct {
Resource coretypes.Resource
Verb coretypes.Verb
IDs ResourceIDsExtractor
Selector SelectorFunc
Related *RelatedResource
}
// RelatedResource is a counterpart named purely for audit clarity. It carries no
// verb and no selector and is not authz-checked.
type RelatedResource struct {
Resource coretypes.Resource
ID ResourceIDExtractor
}
func (ResourceDef) sealResourceSpec() {}
func (ResourcesDef) sealResourceSpec() {}
func (d ResourceDef) validate() error {
if err := coretypes.ErrIfVerbNotValidForResource(d.Verb, *coretypes.NewResourceRef(d.Resource)); err != nil {
return err
}
if d.Related != nil && d.Verb != coretypes.VerbAttach && d.Verb != coretypes.VerbDetach {
return errors.Newf(errors.TypeInvalidInput, errCodeInvalidResourceDef, "Related is only valid with attach/detach, got %s", d.Verb.StringValue())
}
return nil
}
func (d ResourcesDef) validate() error {
if err := coretypes.ErrIfVerbNotValidForResource(d.Verb, *coretypes.NewResourceRef(d.Resource)); err != nil {
return err
}
if d.Related != nil && d.Verb != coretypes.VerbAttach && d.Verb != coretypes.VerbDetach {
return errors.Newf(errors.TypeInvalidInput, errCodeInvalidResourceDef, "Related is only valid with attach/detach, got %s", d.Verb.StringValue())
}
return nil
}
func (d ResourceDef) resolveRequest(ec ExtractorContext) []ResolvedResource {
resolved := ResolvedResource{Resource: d.Resource, Verb: d.Verb, Selector: d.Selector}
resolved.ID, resolved.responseID = resolveID(d.ID, ec)
resolved.Related = resolveRelated(d.Related, ec)
return []ResolvedResource{resolved}
}
func (d ResourcesDef) resolveRequest(ec ExtractorContext) []ResolvedResource {
var ids []string
if d.IDs.fn != nil {
ids, _ = d.IDs.fn(ec)
}
resolved := make([]ResolvedResource, 0, len(ids))
for _, id := range ids {
resolved = append(resolved, ResolvedResource{
Resource: d.Resource,
Verb: d.Verb,
ID: id,
Selector: d.Selector,
Related: resolveRelated(d.Related, ec),
})
}
return resolved
}
// resolveID runs a request-phase extractor immediately and returns its value;
// for a response-phase extractor it returns ("", extractor) so audit can run it
// later.
func resolveID(extractor ResourceIDExtractor, ec ExtractorContext) (string, ResourceIDExtractor) {
if extractor.fn == nil {
return "", ResourceIDExtractor{}
}
if extractor.phase == phaseResponse {
return "", extractor
}
id, _ := extractor.fn(ec)
return id, ResourceIDExtractor{}
}
func resolveRelated(related *RelatedResource, ec ExtractorContext) *ResolvedRelated {
if related == nil {
return nil
}
resolved := &ResolvedRelated{Resource: related.Resource}
resolved.ID, resolved.responseID = resolveID(related.ID, ec)
return resolved
}
// ResolvedResource is the uniform output of resolution (after fan-out). ID is a
// resolved string: request-phase ids are filled by the resource middleware;
// response-phase ids stay "" until FinalizeResponseIDs runs in the audit middleware.
type ResolvedResource struct {
Resource coretypes.Resource
Verb coretypes.Verb
ID string
Selector SelectorFunc
Related *ResolvedRelated
responseID ResourceIDExtractor
}
// ResolvedRelated is the resolved counterpart for audit context.
type ResolvedRelated struct {
Resource coretypes.Resource
ID string
responseID ResourceIDExtractor
}
// ResolveRequest resolves the request-phase ids for all specs (fan-out included)
// against ec. Called by the resource middleware pre-handler.
func ResolveRequest(defs []ResourceSpec, ec ExtractorContext) []ResolvedResource {
var resolved []ResolvedResource
for _, def := range defs {
resolved = append(resolved, def.resolveRequest(ec)...)
}
return resolved
}
// FinalizeResponseIDs runs the carried response-phase extractors against ec to
// fill the ids that were unknown pre-handler. Called by the audit middleware
// post-handler. Mutates the entries in place.
func FinalizeResponseIDs(resolved []ResolvedResource, ec ExtractorContext) {
for idx := range resolved {
if resolved[idx].responseID.fn != nil {
resolved[idx].ID, _ = resolved[idx].responseID.fn(ec)
}
if resolved[idx].Related != nil && resolved[idx].Related.responseID.fn != nil {
resolved[idx].Related.ID, _ = resolved[idx].Related.responseID.fn(ec)
}
}
}
// HasResponseIDs reports whether any resolved entry needs the response body to
// finalize its id. The audit middleware uses this to decide whether to capture
// the success response body.
func HasResponseIDs(resolved []ResolvedResource) bool {
for idx := range resolved {
if resolved[idx].responseID.fn != nil {
return true
}
if resolved[idx].Related != nil && resolved[idx].Related.responseID.fn != nil {
return true
}
}
return false
}

View File

@@ -0,0 +1,100 @@
package handler
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResourceDefValidate(t *testing.T) {
// Related on a lifecycle verb is rejected.
err := ResourceDef{Resource: coretypes.ResourceServiceAccount, Verb: coretypes.VerbCreate, Related: &RelatedResource{Resource: coretypes.ResourceRole}}.validate()
require.Error(t, err)
// Attach is valid for service account and may carry Related.
err = ResourceDef{Resource: coretypes.ResourceServiceAccount, Verb: coretypes.VerbAttach, Related: &RelatedResource{Resource: coretypes.ResourceRole}}.validate()
require.NoError(t, err)
// Verb not valid for resource: factor-api-key disallows attach.
err = ResourceDef{Resource: coretypes.ResourceMetaResourceFactorAPIKey, Verb: coretypes.VerbAttach}.validate()
require.Error(t, err)
}
func TestStandardSelectors(t *testing.T) {
ctx := context.Background()
claims := authtypes.Claims{}
wildcard, err := WildcardSelector(ctx, coretypes.ResourceServiceAccount, "ignored", claims)
require.NoError(t, err)
require.Len(t, wildcard, 1)
assert.Equal(t, coretypes.WildCardSelectorString, wildcard[0].String())
// IDSelector errors on a missing id — no silent wildcard fallback.
_, err = IDSelector(ctx, coretypes.ResourceServiceAccount, "", claims)
require.Error(t, err)
id := "0199c47d-f61b-7833-bc5f-c0730f12f046"
selectors, err := IDSelector(ctx, coretypes.ResourceServiceAccount, id, claims)
require.NoError(t, err)
require.Len(t, selectors, 2)
assert.Equal(t, id, selectors[0].String())
assert.Equal(t, coretypes.WildCardSelectorString, selectors[1].String())
}
func TestResolveRequestAndFinalize(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/x", nil)
req = mux.SetURLVars(req, map[string]string{"id": "sa-1"})
body := []byte(`{"id":"role-1","channels":["c1","c2"]}`)
defs := []ResourceSpec{
ResourceDef{
Resource: coretypes.ResourceServiceAccount, Verb: coretypes.VerbAttach,
ID: PathParam("id"), Selector: IDSelector,
Related: &RelatedResource{Resource: coretypes.ResourceRole, ID: BodyJSONPath("id")},
},
ResourceDef{
Resource: coretypes.ResourceMetaResourceFactorAPIKey, Verb: coretypes.VerbCreate,
ID: ResponseJSONPath("data.id"), Selector: WildcardSelector,
},
ResourcesDef{
Resource: coretypes.ResourceMetaResourceNotificationChannel, Verb: coretypes.VerbAttach,
IDs: BodyJSONArray("channels"), Selector: IDSelector,
},
}
resolved := ResolveRequest(defs, ExtractorContext{Request: req, RequestBody: body})
// 1 service account + 1 create + 2 channels (fan-out).
require.Len(t, resolved, 4)
assert.Equal(t, "sa-1", resolved[0].ID)
require.NotNil(t, resolved[0].Related)
assert.Equal(t, "role-1", resolved[0].Related.ID)
assert.Equal(t, "", resolved[1].ID, "response-phase id is empty pre-handler")
assert.Equal(t, "c1", resolved[2].ID)
assert.Equal(t, "c2", resolved[3].ID)
assert.True(t, HasResponseIDs(resolved))
// Audit finalizes the response-phase id once the response body is present.
FinalizeResponseIDs(resolved, ExtractorContext{ResponseBody: []byte(`{"data":{"id":"key-9"}}`)})
assert.Equal(t, "key-9", resolved[1].ID)
}
func TestExtractorPhases(t *testing.T) {
assert.Equal(t, phaseRequest, PathParam("id").phase)
assert.Equal(t, phaseRequest, BodyJSONPath("id").phase)
assert.Equal(t, phaseRequest, BodyJSONArray("ids").phase)
assert.Equal(t, phaseResponse, ResponseJSONPath("data.id").phase)
// ResponseJSONPath yields "" when the response body is absent (pre-handler).
id, err := ResponseJSONPath("data.id").fn(ExtractorContext{})
require.NoError(t, err)
assert.Equal(t, "", id)
}

View File

@@ -61,6 +61,13 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
responseBuffer := &byteBuffer{}
writer := newResponseCapture(rw, responseBuffer)
// If any resolved resource derives its id from the response, capture the
// success body (bounded) so the audit event can read it post-handler.
if resolved, ok := ResolvedFromContext(req.Context()); ok && handler.HasResponseIDs(*resolved) {
writer.EnableBodyCapture()
}
next.ServeHTTP(writer, req)
statusCode, writeErr := writer.StatusCode(), writer.WriteError()
@@ -80,7 +87,9 @@ func (middleware *Audit) Wrap(next http.Handler) http.Handler {
fields = append(fields, errors.Attr(writeErr))
middleware.logger.ErrorContext(req.Context(), logMessage, fields...)
} else {
if responseBuffer.Len() != 0 {
// Only log error bodies (status >= 400); a force-captured success
// body is for audit id extraction, not for logging.
if statusCode >= 400 && responseBuffer.Len() != 0 {
fields = append(fields, "response.body", responseBuffer.String())
}
@@ -94,76 +103,44 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
return
}
def := auditDefFromRequest(req)
if def == nil {
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,
)
// Resources resolved by the Resource middleware — emit one event per entry.
resolved, ok := ResolvedFromContext(req.Context())
if !ok || len(*resolved) == 0 {
return
}
middleware.auditor.Audit(req.Context(), event)
}
func auditDefFromRequest(req *http.Request) *handler.AuditDef {
route := mux.CurrentRoute(req)
if route == nil {
return nil
}
actualHandler := route.GetHandler()
if actualHandler == nil {
return nil
}
// The type assertion is necessary because route.GetHandler() returns
// http.Handler, and not every http.Handler on the mux is a handler.Handler
// (e.g. middleware wrappers, raw http.HandlerFunc registrations).
provider, ok := actualHandler.(handler.Handler)
if !ok {
return nil
}
return provider.AuditDef()
}
func resourceIDFromRequest(req *http.Request, param string) string {
if param == "" {
return ""
}
vars := mux.Vars(req)
if vars == nil {
return ""
}
return vars[param]
handler.FinalizeResponseIDs(*resolved, handler.ExtractorContext{Request: req, ResponseBody: writer.BodyBytes()})
for _, entry := range *resolved {
resourceAttributes := audittypes.NewResourceAttributes(entry.Resource, entry.ID)
if entry.Related != nil {
resourceAttributes = audittypes.NewAttachResourceAttributes(entry.Resource, entry.ID, entry.Related.Resource, entry.Related.ID)
}
event := audittypes.NewAuditEvent(
req,
routeTemplate,
statusCode,
span.SpanContext().TraceID(),
span.SpanContext().SpanID(),
entry.Verb,
audittypes.CategoryFor(entry.Resource),
claims,
resourceAttributes,
errorType,
errorCode,
)
middleware.auditor.Audit(req.Context(), event)
}
}

View File

@@ -285,6 +285,67 @@ func (middleware *AuthZ) CheckAll(next http.HandlerFunc, groups []AuthZCheckGrou
})
}
// CheckResources authorizes every resolved ResourceDef for the route (AND across
// defs). It reads the list placed by the Resource middleware. Each def's Selector
// is the sole source of its FGA selectors; roles are the role names allowed
// (consumed by the OSS role-gate, while the resource selectors drive the EE
// resource check).
func (middleware *AuthZ) CheckResources(next http.HandlerFunc, roles ...string) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
resolved, ok := ResolvedFromContext(ctx)
if !ok {
render.Error(rw, errors.New(errors.TypeInternal, errors.CodeInternal, "resource defs not resolved; CheckResources requires WithResourceDefs on the route"))
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
roleSelectors := make([]coretypes.Selector, len(roles))
for idx, role := range roles {
roleSelectors[idx] = coretypes.TypeRole.MustSelector(role)
}
for _, def := range *resolved {
if def.Selector == nil {
render.Error(rw, errors.New(errors.TypeInternal, errors.CodeInternal, "resource def used with CheckResources must declare a Selector"))
return
}
selectors, err := def.Selector(ctx, def.Resource, def.ID, claims)
if err != nil {
render.Error(rw, err)
return
}
err = middleware.authzService.CheckWithTupleCreation(ctx, claims, orgID, authtypes.Relation{Verb: def.Verb}, def.Resource, selectors, roleSelectors)
if err != nil {
if errors.Asc(err, authtypes.ErrCodeAuthZForbidden) {
middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims))
if def.ID != "" {
render.Error(rw, errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "you don't have %s access to %s %s", def.Verb.StringValue(), def.Resource.Kind().String(), def.ID))
return
}
render.Error(rw, errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "you don't have %s access to %s", def.Verb.StringValue(), def.Resource.Kind().String()))
return
}
render.Error(rw, err)
return
}
}
next(rw, req)
})
}
func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, typeable coretypes.Resource, cb selectorCallbackWithoutClaimsFn, roles []string) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()

View File

@@ -0,0 +1,81 @@
package middleware
import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/gorilla/mux"
)
// resolvedKey is the context key under which the resolved resource list is stored.
type resolvedKey struct{}
// Resource resolves a route's declared ResourceDefs (request-side) and stashes
// the result in the request context. It is the OUTER of the resource-aware
// middlewares (placed before Audit) and the single point that buffers the
// request body. AuthZ (in the handler) and Audit (inner) read the resolved list.
type Resource struct {
logger *slog.Logger
}
func NewResource(logger *slog.Logger) *Resource {
return &Resource{logger: logger.With(slog.String("pkg", pkgname))}
}
func (middleware *Resource) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
defs := resourceDefsFromRequest(req)
if len(defs) == 0 {
next.ServeHTTP(rw, req)
return
}
// Buffer the request body once so request-side extractors can read it and
// the handler still sees a fresh reader. Single buffering point.
var body []byte
if req.Body != nil {
body, _ = io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewReader(body))
}
resolved := handler.ResolveRequest(defs, handler.ExtractorContext{Request: req, RequestBody: body})
ctx := withResolved(req.Context(), &resolved)
next.ServeHTTP(rw, req.WithContext(ctx))
})
}
func resourceDefsFromRequest(req *http.Request) []handler.ResourceSpec {
route := mux.CurrentRoute(req)
if route == nil {
return nil
}
actualHandler := route.GetHandler()
if actualHandler == nil {
return nil
}
provider, ok := actualHandler.(handler.Handler)
if !ok {
return nil
}
return provider.ResourceDefs()
}
func withResolved(ctx context.Context, resolved *[]handler.ResolvedResource) context.Context {
return context.WithValue(ctx, resolvedKey{}, resolved)
}
// ResolvedFromContext returns the resolved resource list placed by the Resource
// middleware. The pointer lets the audit middleware finalize response-phase ids
// in place.
func ResolvedFromContext(ctx context.Context) (*[]handler.ResolvedResource, bool) {
resolved, ok := ctx.Value(resolvedKey{}).(*[]handler.ResolvedResource)
return resolved, ok
}

View File

@@ -23,9 +23,14 @@ type responseCapture interface {
// WriteError returns the error (if any) from the downstream Write call.
WriteError() error
// BodyBytes returns the captured response body bytes. Only populated
// for error responses (status >= 400).
// BodyBytes returns the captured response body bytes. Populated for error
// responses (status >= 400), or for any response once EnableBodyCapture is called.
BodyBytes() []byte
// EnableBodyCapture forces capture of the response body regardless of status
// code (still bounded by maxResponseBodyCapture). Must be called before the
// handler writes the response.
EnableBodyCapture()
}
func newResponseCapture(rw http.ResponseWriter, buffer *byteBuffer) responseCapture {
@@ -72,12 +77,13 @@ func (b *byteBuffer) String() string {
}
type nonFlushingResponseCapture struct {
rw http.ResponseWriter
buffer *byteBuffer
captureBody bool
bodyBytesLeft int
statusCode int
writeError error
rw http.ResponseWriter
buffer *byteBuffer
captureBody bool
forceCaptureBody bool
bodyBytesLeft int
statusCode int
writeError error
}
type flushingResponseCapture struct {
@@ -98,13 +104,17 @@ func (writer *nonFlushingResponseCapture) Header() http.Header {
// WriteHeader writes the HTTP response header.
func (writer *nonFlushingResponseCapture) WriteHeader(statusCode int) {
writer.statusCode = statusCode
if statusCode >= 400 {
if statusCode >= 400 || writer.forceCaptureBody {
writer.captureBody = true
}
writer.rw.WriteHeader(statusCode)
}
func (writer *nonFlushingResponseCapture) EnableBodyCapture() {
writer.forceCaptureBody = true
}
// Write writes HTTP response data.
func (writer *nonFlushingResponseCapture) Write(data []byte) (int, error) {
if writer.statusCode == 0 {

View File

@@ -168,6 +168,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewResource(s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewComment().Wrap)

View File

@@ -71,23 +71,65 @@ func (attributes PrincipalAttributes) Put(dest pcommon.Map) {
// Audit attributes — Resource (On What).
// These are OTel resource attributes (placed on the Resource, not event attributes).
type ResourceAttributes struct {
ResourceID string
ResourceKind coretypes.Kind // guaranteed to be present
Resource coretypes.Resource // guaranteed to be present
ResourceID string
// TargetResource names the counterpart of an attach/detach event (audit
// context only). nil when there is no relationship.
TargetResource coretypes.Resource
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,
}
}
// NewAttachResourceAttributes builds resource attributes that additionally name
// the target counterpart (used for attach/detach audit events).
func NewAttachResourceAttributes(resource coretypes.Resource, resourceID string, targetResource coretypes.Resource, targetResourceID string) ResourceAttributes {
return ResourceAttributes{
Resource: resource,
ResourceID: resourceID,
TargetResource: targetResource,
TargetResourceID: targetResourceID,
}
}
// CategoryFor derives the audit ActionCategory from a resource's kind. Audit owns
// this mapping so ResourceDef stays consumer-agnostic.
func CategoryFor(resource coretypes.Resource) ActionCategory {
switch resource.Kind().String() {
case "role", "serviceaccount", "user", "auth-domain", "session", "factor-password", "factor-api-key":
return ActionCategoryAccessControl
default:
return ActionCategoryConfigurationChange
}
}
// 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())
putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID)
func (attributes ResourceAttributes) PutResource(orgID valuer.UUID, dest pcommon.Map) {
if attributes.Resource != nil {
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.Resource.Kind().String())
putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID)
if attributes.ResourceID != "" {
// The FGA object string — correlates with the authz Check() object on
// the same resource (id-keyed resources match exactly).
putStrIfNotEmpty(dest, "signoz.audit.resource.object", attributes.Resource.Object(orgID, attributes.ResourceID))
}
}
if attributes.TargetResource != nil {
putStrIfNotEmpty(dest, "signoz.audit.resource.target.kind", attributes.TargetResource.Kind().String())
putStrIfNotEmpty(dest, "signoz.audit.resource.target.id", attributes.TargetResourceID)
if attributes.TargetResourceID != "" {
putStrIfNotEmpty(dest, "signoz.audit.resource.target.object", attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
}
}
}
// Audit attributes — Error (When outcome is failure)
@@ -192,12 +234,25 @@ func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttri
}
// Resource: " kind (id)" or " kind".
b.WriteString(" ")
b.WriteString(resourceAttributes.ResourceKind.String())
if resourceAttributes.ResourceID != "" {
b.WriteString(" (")
b.WriteString(resourceAttributes.ResourceID)
b.WriteString(")")
if resourceAttributes.Resource != nil {
b.WriteString(" ")
b.WriteString(resourceAttributes.Resource.Kind().String())
if resourceAttributes.ResourceID != "" {
b.WriteString(" (")
b.WriteString(resourceAttributes.ResourceID)
b.WriteString(")")
}
}
// Target (attach/detach context): " · target kind (id)" or " · target kind".
if resourceAttributes.TargetResource != nil {
b.WriteString(" · target ")
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.

View File

@@ -63,8 +63,8 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "",
ResourceKind: coretypes.MustNewKind("dashboard"),
ResourceID: "",
Resource: coretypes.ResourceMetaResourceDashboard,
},
errorAttributes: ErrorAttributes{},
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) deleted dashboard",
@@ -81,8 +81,8 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.Email{},
},
resourceAttributes: ResourceAttributes{
ResourceID: "abd",
ResourceKind: coretypes.MustNewKind("dashboard"),
ResourceID: "abd",
Resource: coretypes.ResourceMetaResourceDashboard,
},
errorAttributes: ErrorAttributes{},
expectedBody: "019a1234-abcd-7000-8000-567800000001 deleted dashboard (abd)",
@@ -99,8 +99,8 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.Email{},
},
resourceAttributes: ResourceAttributes{
ResourceID: "abd",
ResourceKind: coretypes.MustNewKind("dashboard"),
ResourceID: "abd",
Resource: coretypes.ResourceMetaResourceDashboard,
},
errorAttributes: ErrorAttributes{},
expectedBody: "deleted dashboard (abd)",
@@ -117,8 +117,8 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.MustNewEmail("alice@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "019b-5678",
ResourceKind: coretypes.MustNewKind("dashboard"),
ResourceID: "019b-5678",
Resource: coretypes.ResourceMetaResourceDashboard,
},
errorAttributes: ErrorAttributes{},
expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678)",
@@ -132,10 +132,10 @@ func TestNewBody(t *testing.T) {
},
principalAttributes: PrincipalAttributes{},
resourceAttributes: ResourceAttributes{
ResourceKind: coretypes.MustNewKind("alert-rule"),
Resource: coretypes.ResourceMetaResourceRule,
},
errorAttributes: ErrorAttributes{},
expectedBody: "updated alert-rule",
expectedBody: "updated rule",
},
{
name: "Failure_AllPresent",
@@ -149,8 +149,8 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.MustNewEmail("viewer@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "019b-5678",
ResourceKind: coretypes.MustNewKind("dashboard"),
ResourceID: "019b-5678",
Resource: coretypes.ResourceMetaResourceDashboard,
},
errorAttributes: ErrorAttributes{
ErrorType: "forbidden",
@@ -169,7 +169,7 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceKind: coretypes.MustNewKind("user"),
Resource: coretypes.ResourceUser,
},
errorAttributes: ErrorAttributes{
ErrorType: "not-found",
@@ -187,8 +187,8 @@ func TestNewBody(t *testing.T) {
PrincipalEmail: valuer.MustNewEmail("test@acme.com"),
},
resourceAttributes: ResourceAttributes{
ResourceID: "019b-5678",
ResourceKind: coretypes.MustNewKind("dashboard"),
ResourceID: "019b-5678",
Resource: coretypes.ResourceMetaResourceDashboard,
},
errorAttributes: ErrorAttributes{},
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to create dashboard (019b-5678)",

View File

@@ -0,0 +1,26 @@
package audittypes
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/stretchr/testify/assert"
)
func TestCategoryFor(t *testing.T) {
assert.Equal(t, ActionCategoryAccessControl, CategoryFor(coretypes.ResourceServiceAccount))
assert.Equal(t, ActionCategoryAccessControl, CategoryFor(coretypes.ResourceRole))
assert.Equal(t, ActionCategoryAccessControl, CategoryFor(coretypes.ResourceMetaResourceFactorAPIKey))
assert.Equal(t, ActionCategoryConfigurationChange, CategoryFor(coretypes.ResourceMetaResourceDashboard))
assert.Equal(t, ActionCategoryConfigurationChange, CategoryFor(coretypes.ResourceMetaResourceNotificationChannel))
}
func TestNewBodyWithTarget(t *testing.T) {
auditAttributes := AuditAttributes{Action: coretypes.VerbAttach, Outcome: OutcomeSuccess}
principalAttributes := PrincipalAttributes{}
resourceAttributes := NewAttachResourceAttributes(coretypes.ResourceServiceAccount, "sa-1", coretypes.ResourceMetaResourceFactorAPIKey, "key-9")
body := newBody(auditAttributes, principalAttributes, resourceAttributes, ErrorAttributes{})
assert.Contains(t, body, "serviceaccount (sa-1)")
assert.Contains(t, body, "target factor-api-key (key-9)")
}

View File

@@ -44,7 +44,9 @@ type AuditEvent struct {
TransportAttributes TransportAttributes
}
func NewAuditEventFromHTTPRequest(
// NewAuditEvent builds an audit event from pre-built resource attributes (which
// may carry attach/target context).
func NewAuditEvent(
req *http.Request,
route string,
statusCode int,
@@ -53,14 +55,12 @@ func NewAuditEventFromHTTPRequest(
action 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)
principalAttributes := NewPrincipalAttributesFromClaims(claims)
resourceAttributes := NewResourceAttributes(resourceID, resourceKind)
errorAttributes := NewErrorAttributes(errorType, errorCode)
transportAttributes := NewTransportAttributesFromHTTP(req, route, statusCode)
@@ -69,7 +69,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.Action),
AuditAttributes: auditAttributes,
PrincipalAttributes: principalAttributes,
ResourceAttributes: resourceAttributes,
@@ -89,7 +89,11 @@ 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}
kind := ""
if event.ResourceAttributes.Resource != nil {
kind = event.ResourceAttributes.Resource.Kind().String()
}
key := resourceKey{kind: kind, id: event.ResourceAttributes.ResourceID}
if _, exists := groups[key]; !exists {
order = append(order, key)
}
@@ -101,7 +105,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)
first := events[groups[key][0]]
first.ResourceAttributes.PutResource(first.PrincipalAttributes.PrincipalOrgID, resourceAttrs)
scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
scopeLogs.Scope().SetName(scope)

View File

@@ -12,10 +12,10 @@ import (
)
var (
testDashboardKind = coretypes.MustNewKind("dashboard")
testDashboardResource = coretypes.ResourceMetaResourceDashboard
)
func TestNewAuditEventFromHTTPRequest(t *testing.T) {
func TestNewAuditEvent(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}
@@ -28,8 +28,8 @@ func TestNewAuditEventFromHTTPRequest(t *testing.T) {
action coretypes.Verb
category ActionCategory
claims authtypes.Claims
resource coretypes.Resource
resourceID string
resourceKind coretypes.Kind
errorType string
errorCode string
expectedOutcome Outcome
@@ -44,8 +44,8 @@ func TestNewAuditEventFromHTTPRequest(t *testing.T) {
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},
resource: testDashboardResource,
resourceID: "019b-5678-efgh-9012",
resourceKind: testDashboardKind,
expectedOutcome: OutcomeSuccess,
expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678-efgh-9012)",
},
@@ -58,8 +58,8 @@ func TestNewAuditEventFromHTTPRequest(t *testing.T) {
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},
resource: testDashboardResource,
resourceID: "019b-5678-efgh-9012",
resourceKind: testDashboardKind,
errorType: "forbidden",
errorCode: "authz_forbidden",
expectedOutcome: OutcomeFailure,
@@ -71,7 +71,7 @@ func TestNewAuditEventFromHTTPRequest(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
req := httptest.NewRequest(testCase.method, testCase.path, nil)
event := NewAuditEventFromHTTPRequest(
event := NewAuditEvent(
req,
testCase.route,
testCase.statusCode,
@@ -80,15 +80,14 @@ func TestNewAuditEventFromHTTPRequest(t *testing.T) {
testCase.action,
testCase.category,
testCase.claims,
testCase.resourceID,
testCase.resourceKind,
NewResourceAttributes(testCase.resource, testCase.resourceID),
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.resource.Kind(), event.ResourceAttributes.Resource.Kind())
assert.Equal(t, testCase.resourceID, event.ResourceAttributes.ResourceID)
assert.Equal(t, testCase.action, event.AuditAttributes.Action)
assert.Equal(t, testCase.category, event.AuditAttributes.ActionCategory)
@@ -103,18 +102,18 @@ func TestNewAuditEventFromHTTPRequest(t *testing.T) {
}
}
func newTestEvent(resourceKind coretypes.Kind, resourceID string, action coretypes.Verb) AuditEvent {
func newTestEvent(resource coretypes.Resource, resourceID string, action coretypes.Verb) AuditEvent {
return AuditEvent{
Body: resourceKind.String() + "." + action.PastTense(),
EventName: NewEventName(resourceKind, action),
Body: resource.Kind().String() + "." + action.PastTense(),
EventName: NewEventName(resource.Kind(), action),
AuditAttributes: AuditAttributes{
Action: action,
ActionCategory: ActionCategoryConfigurationChange,
Outcome: OutcomeSuccess,
},
ResourceAttributes: ResourceAttributes{
ResourceKind: resourceKind,
ResourceID: resourceID,
Resource: resource,
ResourceID: resourceID,
},
}
}
@@ -136,7 +135,7 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
{
name: "SingleEvent",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbCreate),
},
expectedResourceLogs: 1,
expectedResourceKinds: []string{"dashboard"},
@@ -146,9 +145,9 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
{
name: "SameResource_MultipleEvents",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbCreate),
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(testDashboardKind, "d-001", coretypes.VerbDelete),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbCreate),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbUpdate),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbDelete),
},
expectedResourceLogs: 1,
expectedResourceKinds: []string{"dashboard"},
@@ -158,8 +157,8 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
{
name: "DifferentResources_SeparateGroups",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(coretypes.MustNewKind("user"), "u-001", coretypes.VerbDelete),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbUpdate),
newTestEvent(coretypes.ResourceUser, "u-001", coretypes.VerbDelete),
},
expectedResourceLogs: 2,
expectedResourceKinds: []string{"dashboard", "user"},
@@ -169,8 +168,8 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
{
name: "SameKind_DifferentIDs_SeparateGroups",
events: []AuditEvent{
newTestEvent(testDashboardKind, "d-001", coretypes.VerbUpdate),
newTestEvent(testDashboardKind, "d-002", coretypes.VerbDelete),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbUpdate),
newTestEvent(testDashboardResource, "d-002", coretypes.VerbDelete),
},
expectedResourceLogs: 2,
expectedResourceKinds: []string{"dashboard", "dashboard"},
@@ -180,11 +179,11 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
{
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),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbCreate),
newTestEvent(coretypes.ResourceUser, "u-001", coretypes.VerbUpdate),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbUpdate),
newTestEvent(coretypes.ResourceUser, "u-001", coretypes.VerbDelete),
newTestEvent(testDashboardResource, "d-001", coretypes.VerbDelete),
},
expectedResourceLogs: 2,
expectedResourceKinds: []string{"dashboard", "user"},
@@ -203,7 +202,6 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
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())
@@ -212,7 +210,6 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
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())
@@ -221,14 +218,11 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
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")