MG-370 - Add fine grained access control to reports (#403)

* add access control to rules engine

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix build

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused variable

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix report database

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix variable naming

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix entity type

Signed-off-by: Arvindh <arvindh91@gmail.com>

* update authorize method

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix generate report

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* revert env changes

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update generate permission

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* revert go mod file

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* revert go mod file

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

---------

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Co-authored-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Steve Munene
2026-03-05 15:59:22 +03:00
committed by GitHub
parent 362a4fc76d
commit 178a62c08f
28 changed files with 4329 additions and 260 deletions
+136 -6
View File
@@ -19,40 +19,57 @@ import (
"github.com/absmach/magistrala/internal/email"
"github.com/absmach/magistrala/pkg/emailer"
pkglog "github.com/absmach/magistrala/pkg/logger"
"github.com/absmach/magistrala/pkg/prometheus"
"github.com/absmach/magistrala/pkg/ticker"
grpcClient "github.com/absmach/magistrala/readers/api/grpc"
"github.com/absmach/magistrala/reports"
httpapi "github.com/absmach/magistrala/reports/api"
"github.com/absmach/magistrala/reports/middleware"
"github.com/absmach/magistrala/reports/operations"
repg "github.com/absmach/magistrala/reports/postgres"
"github.com/absmach/supermq"
dpostgres "github.com/absmach/supermq/domains/postgres"
smqlog "github.com/absmach/supermq/logger"
smqauthn "github.com/absmach/supermq/pkg/authn"
authnsvc "github.com/absmach/supermq/pkg/authn/authsvc"
mgauthz "github.com/absmach/supermq/pkg/authz"
authzsvc "github.com/absmach/supermq/pkg/authz/authsvc"
"github.com/absmach/supermq/pkg/callout"
dconsumer "github.com/absmach/supermq/pkg/domains/events/consumer"
domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient"
"github.com/absmach/supermq/pkg/grpcclient"
jaegerclient "github.com/absmach/supermq/pkg/jaeger"
"github.com/absmach/supermq/pkg/permissions"
"github.com/absmach/supermq/pkg/policies"
"github.com/absmach/supermq/pkg/policies/spicedb"
pgclient "github.com/absmach/supermq/pkg/postgres"
"github.com/absmach/supermq/pkg/roles"
"github.com/absmach/supermq/pkg/server"
httpserver "github.com/absmach/supermq/pkg/server/http"
spicedbdecoder "github.com/absmach/supermq/pkg/spicedb"
"github.com/absmach/supermq/pkg/uuid"
"github.com/authzed/authzed-go/v1"
"github.com/authzed/grpcutil"
"github.com/caarlos0/env/v11"
"github.com/go-chi/chi/v5"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
svcName = "reports"
envPrefixDB = "MG_REPORTS_DB_"
envPrefixHTTP = "MG_REPORTS_HTTP_"
envPrefixCallout = "MG_REPORTS_CALLOUT_"
envPrefixAuth = "SMQ_AUTH_GRPC_"
defDB = "repo"
defSvcHTTPPort = "9017"
envPrefixGrpc = "MG_TIMESCALE_READER_GRPC_"
envPrefixDomains = "SMQ_DOMAINS_GRPC_"
templatePath = "template/reports_default_template.html"
reportEntity = "report"
)
// We use a buffered channel to prevent blocking, as logging is an expensive operation.
@@ -67,10 +84,16 @@ type config struct {
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
ESConsumerName string `env:"MG_REPORTS_EVENT_CONSUMER" envDefault:"reports"`
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
DefaultTemplatePath string `env:"MG_REPORTS_DEFAULT_TEMPLATE" envDefault:""`
ConverterURL string `env:"MG_PDF_CONVERTER_URL" envDefault:"http://localhost:4000/pdf"`
SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"`
SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"`
SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"`
SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"schema.zed"`
PermissionsFile string `env:"SMQ_PERMISSIONS_FILE" envDefault:"permission.yaml"`
}
func main() {
@@ -131,6 +154,13 @@ func main() {
return
}
callCfg := callout.Config{}
if err := env.ParseWithOptions(&callCfg, env.Options{Prefix: envPrefixCallout}); err != nil {
logger.Error(fmt.Sprintf("failed to parse callout config : %s", err))
exitCode = 1
return
}
dbConfig := pgclient.Config{Name: defDB}
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
logger.Error(err.Error())
@@ -139,7 +169,15 @@ func main() {
return
}
db, err := pgclient.Setup(dbConfig, *repg.Migration())
migration, err := repg.Migration()
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
db, err := pgclient.Setup(dbConfig, *migration)
if err != nil {
logger.Error(err.Error())
exitCode = 1
@@ -170,6 +208,13 @@ func main() {
return
}
callout, err := callout.New(callCfg)
if err != nil {
logger.Error(fmt.Sprintf("failed to create new callout: %s", err))
exitCode = 1
return
}
grpcCfg := grpcclient.Config{}
if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil {
logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err))
@@ -211,6 +256,15 @@ func main() {
defer authzClient.Close()
logger.Info("AuthZ successfully connected to auth gRPC server " + authnClient.Secure())
ddatabase := pgclient.NewDatabase(db, dbConfig, tracer)
drepo := dpostgres.NewRepository(ddatabase)
if err := dconsumer.DomainsEventsSubscribe(ctx, drepo, cfg.ESURL, cfg.ESConsumerName, logger); err != nil {
logger.Error(fmt.Sprintf("failed to create domains event store : %s", err))
exitCode = 1
return
}
database := pgclient.NewDatabase(db, dbConfig, tracer)
regrpcCfg := grpcclient.Config{}
if err := env.ParseWithOptions(&regrpcCfg, env.Options{Prefix: envPrefixGrpc}); err != nil {
@@ -231,7 +285,7 @@ func main() {
runInfo := make(chan pkglog.RunInfo, channBuffer)
svc, err := newService(database, runInfo, authz, ec, logger, readersClient, template, cfg.ConverterURL)
svc, err := newService(cfg, database, runInfo, authz, ec, logger, readersClient, template, callout, tracer)
if err != nil {
logger.Error(fmt.Sprintf("failed to create services: %s", err))
exitCode = 1
@@ -271,21 +325,97 @@ func main() {
}
}
func newService(db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, converterURL string) (reports.Service, error) {
func newService(cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, callout callout.Callout, tracer trace.Tracer) (reports.Service, error) {
repo := repg.NewRepository(db)
idp := uuid.New()
emailerClient, err := emailer.New(&ec)
emailClient, err := emailer.New(&ec)
if err != nil {
logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error()))
}
csvc := reports.NewService(repo, runInfo, idp, ticker.NewTicker(time.Second*30), emailerClient, readersClient, template, converterURL)
csvc, err = middleware.AuthorizationMiddleware(csvc, authz)
policyService, err := newSpiceDBPolicyServiceEvaluator(cfg, logger)
if err != nil {
return nil, err
}
logger.Info("Policy service successfully connected to SpiceDB gRPC server")
availableActions, builtInRoles, err := availableActionsAndBuiltInRoles(cfg.SpicedbSchemaFile)
if err != nil {
return nil, fmt.Errorf("failed to get available actions and built-in roles: %w", err)
}
csvc, err := reports.NewService(repo, runInfo, policyService, idp, ticker.NewTicker(time.Second*30), emailClient, readersClient, template, cfg.ConverterURL, availableActions, builtInRoles)
if err != nil {
return nil, fmt.Errorf("failed to create reports service: %w", err)
}
permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile)
if err != nil {
return nil, fmt.Errorf("failed to parse permissions file: %w", err)
}
reportOps, reportRoleOps, err := permConfig.GetEntityPermissions(reportEntity)
if err != nil {
return nil, fmt.Errorf("failed to get report permissions: %w", err)
}
entitiesOps, err := permissions.NewEntitiesOperations(
permissions.EntitiesPermission{
operations.EntityType: reportOps,
},
permissions.EntitiesOperationDetails[permissions.Operation]{
operations.EntityType: operations.OperationDetails(),
},
)
if err != nil {
return nil, fmt.Errorf("failed to create entities operations: %w", err)
}
roleOps, err := permissions.NewOperations(roles.Operations(), reportRoleOps)
if err != nil {
return nil, fmt.Errorf("failed to create role operations: %w", err)
}
csvc, err = middleware.AuthorizationMiddleware(csvc, authz, entitiesOps, roleOps)
if err != nil {
return nil, err
}
csvc, err = middleware.NewCallout(csvc, callout, entitiesOps, roleOps)
if err != nil {
return nil, err
}
csvc = middleware.LoggingMiddleware(csvc, logger)
counter, latency := prometheus.MakeMetrics("reports", "api")
csvc = middleware.NewMetricsMiddleware(counter, latency, csvc)
csvc = middleware.NewTracingMiddleware(tracer, csvc)
return csvc, nil
}
func newSpiceDBPolicyServiceEvaluator(cfg config, logger *slog.Logger) (policies.Service, error) {
client, err := authzed.NewClientWithExperimentalAPIs(
fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey),
)
if err != nil {
return nil, err
}
ps := spicedb.NewPolicyService(client, logger)
return ps, nil
}
func availableActionsAndBuiltInRoles(spicedbSchemaFile string) ([]roles.Action, map[roles.BuiltInRoleName][]roles.Action, error) {
availableActions, err := spicedbdecoder.GetActionsFromSchema(spicedbSchemaFile, reportEntity)
if err != nil {
return []roles.Action{}, map[roles.BuiltInRoleName][]roles.Action{}, err
}
builtInRoles := map[roles.BuiltInRoleName][]roles.Action{
reports.BuiltInRoleAdmin: availableActions,
}
return availableActions, builtInRoles, err
}
+1 -1
View File
@@ -477,4 +477,4 @@ MG_RELEASE_TAG=latest
SMQ_ALLOW_UNVERIFIED_USER=true
# Set to yes to accept the EULA for the UI services. To view the EULA visit: https://github.com/absmach/eula
MG_UI_DOCKER_ACCEPT_EULA=no
MG_UI_DOCKER_ACCEPT_EULA=yes
+1
View File
@@ -475,6 +475,7 @@ services:
MG_REPORTS_DEFAULT_TEMPLATE: ${MG_REPORTS_DEFAULT_TEMPLATE}
MG_PDF_CONVERTER_URL: ${MG_PDF_CONVERTER_URL}
SMQ_MESSAGE_BROKER_URL: ${SMQ_MESSAGE_BROKER_URL}
SMQ_ES_URL: ${SMQ_ES_URL}
SMQ_JAEGER_URL: ${SMQ_JAEGER_URL}
SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
+1 -1
View File
@@ -43,7 +43,7 @@ report:
operations:
- add: report_create_permission
- list: report_read_permission
- generate: report_create_permission
- generate: report_read_permission
- view: read_permission
- update: update_permission
- update_schedule: update_permission
+4 -1
View File
@@ -3,4 +3,7 @@
package policies
const RulesType = "rules"
const (
RulesType = "rules"
ReportsType = "reports"
)
+1 -3
View File
@@ -3,9 +3,7 @@
package operations
import (
"github.com/absmach/supermq/pkg/permissions"
)
import "github.com/absmach/supermq/pkg/permissions"
const EntityType = "rule"
+130 -1
View File
@@ -330,7 +330,136 @@ func TestAddRule(t *testing.T) {
Time: now,
},
},
err: re.ErrGoroutinesNotAllowed,
err: re.ErrGoroutinesNotAllowed,
addPoliciesErr: nil,
addRoleErr: nil,
deleteErr: nil,
},
{
desc: "Add rule with failed to add roles and failed to delete policies",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
rule: re.Rule{
Name: ruleName,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
},
res: re.Rule{
Name: ruleName,
ID: ruleID,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
Status: re.EnabledStatus,
CreatedBy: userID,
DomainID: domainID,
},
addRoleErr: svcerr.ErrCreateEntity,
deletePolicies: svcerr.ErrRemoveEntity,
err: svcerr.ErrRemoveEntity,
},
{
desc: "Add rule with failed to add policies",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
rule: re.Rule{
Name: ruleName,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
},
res: re.Rule{
Name: ruleName,
ID: ruleID,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
Status: re.EnabledStatus,
CreatedBy: userID,
DomainID: domainID,
},
addPoliciesErr: svcerr.ErrAuthorization,
err: svcerr.ErrAddPolicies,
},
{
desc: "Add rule with failed to add policies and failed rollback",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
rule: re.Rule{
Name: ruleName,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
},
res: re.Rule{
Name: ruleName,
ID: ruleID,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
Status: re.EnabledStatus,
CreatedBy: userID,
DomainID: domainID,
},
addPoliciesErr: svcerr.ErrAuthorization,
deleteErr: svcerr.ErrRemoveEntity,
err: svcerr.ErrRollbackRepo,
},
{
desc: "Add rule with failed to add roles",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
rule: re.Rule{
Name: ruleName,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
},
res: re.Rule{
Name: ruleName,
ID: ruleID,
InputChannel: inputChannel,
Schedule: pkgSch.Schedule{
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
},
Status: re.EnabledStatus,
CreatedBy: userID,
DomainID: domainID,
},
addRoleErr: svcerr.ErrCreateEntity,
err: svcerr.ErrAddPolicies,
},
}
+1 -1
View File
@@ -155,7 +155,7 @@ func viewReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return viewReportConfigRes{}, err
}
cfg, err := svc.ViewReportConfig(ctx, session, req.ID)
cfg, err := svc.ViewReportConfig(ctx, session, req.ID, req.withRoles)
if err != nil {
return viewReportConfigRes{}, err
}
+1 -2
View File
@@ -333,9 +333,8 @@ func TestViewReportConfigEndpoint(t *testing.T) {
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
svcCall := svc.On("ViewReportConfig", mock.Anything, tc.authnRes, tc.id).Return(tc.svcRes, tc.svcErr)
svcCall := svc.On("ViewReportConfig", mock.Anything, tc.authnRes, tc.id, false).Return(tc.svcRes, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
err = json.NewDecoder(res.Body).Decode(&errRes)
+2 -1
View File
@@ -52,7 +52,8 @@ func (req addReportConfigReq) validate() error {
}
type viewReportConfigReq struct {
ID string `json:"id"`
ID string `json:"id"`
withRoles bool
}
func (req viewReportConfigReq) validate() error {
+72 -59
View File
@@ -17,6 +17,7 @@ import (
apiutil "github.com/absmach/supermq/api/http/util"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
roleManagerHttp "github.com/absmach/supermq/pkg/roles/rolemanager/api"
"github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -38,6 +39,8 @@ func MakeHandler(svc reports.Service, authn smqauthn.AuthNMiddleware, mux *chi.M
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
r.Route("/{domainID}", func(r chi.Router) {
r.Route("/reports", func(r chi.Router) {
d := roleManagerHttp.NewDecoder("reportID")
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
generateReportEndpoint(svc),
decodeGenerateReportRequest,
@@ -45,6 +48,8 @@ func MakeHandler(svc reports.Service, authn smqauthn.AuthNMiddleware, mux *chi.M
opts...,
), "generate_report").ServeHTTP)
r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts)
r.Route("/configs", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
addReportConfigEndpoint(svc),
@@ -53,34 +58,6 @@ func MakeHandler(svc reports.Service, authn smqauthn.AuthNMiddleware, mux *chi.M
opts...,
), "add_report_config").ServeHTTP)
r.Get("/{reportID}", otelhttp.NewHandler(kithttp.NewServer(
viewReportConfigEndpoint(svc),
decodeViewReportConfigRequest,
api.EncodeResponse,
opts...,
), "view_report_config").ServeHTTP)
r.Patch("/{reportID}", otelhttp.NewHandler(kithttp.NewServer(
updateReportConfigEndpoint(svc),
decodeUpdateReportConfigRequest,
api.EncodeResponse,
opts...,
), "update_report_config").ServeHTTP)
r.Patch("/{reportID}/schedule", otelhttp.NewHandler(kithttp.NewServer(
updateReportScheduleEndpoint(svc),
decodeUpdateReportScheduleRequest,
api.EncodeResponse,
opts...,
), "update_report_scheduler").ServeHTTP)
r.Delete("/{reportID}", otelhttp.NewHandler(kithttp.NewServer(
deleteReportConfigEndpoint(svc),
decodeDeleteReportConfigRequest,
api.EncodeResponse,
opts...,
), "delete_report_config").ServeHTTP)
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
listReportsConfigEndpoint(svc),
decodeListReportsConfigRequest,
@@ -88,40 +65,72 @@ func MakeHandler(svc reports.Service, authn smqauthn.AuthNMiddleware, mux *chi.M
opts...,
), "list_reports_config").ServeHTTP)
r.Post("/{reportID}/enable", otelhttp.NewHandler(kithttp.NewServer(
enableReportConfigEndpoint(svc),
decodeUpdateReportStatusRequest,
api.EncodeResponse,
opts...,
), "enable_report_config").ServeHTTP)
r.Route("/{reportID}", func(r chi.Router) {
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
viewReportConfigEndpoint(svc),
decodeViewReportConfigRequest,
api.EncodeResponse,
opts...,
), "view_report_config").ServeHTTP)
r.Post("/{reportID}/disable", otelhttp.NewHandler(kithttp.NewServer(
disableReportConfigEndpoint(svc),
decodeUpdateReportStatusRequest,
api.EncodeResponse,
opts...,
), "disable_report_config").ServeHTTP)
r.Patch("/", otelhttp.NewHandler(kithttp.NewServer(
updateReportConfigEndpoint(svc),
decodeUpdateReportConfigRequest,
api.EncodeResponse,
opts...,
), "update_report_config").ServeHTTP)
r.Put("/{reportID}/template", otelhttp.NewHandler(kithttp.NewServer(
updateReportTemplateEndpoint(svc),
decodeUpdateReportTemplateRequest,
api.EncodeResponse,
opts...,
), "update_report_template").ServeHTTP)
r.Patch("/schedule", otelhttp.NewHandler(kithttp.NewServer(
updateReportScheduleEndpoint(svc),
decodeUpdateReportScheduleRequest,
api.EncodeResponse,
opts...,
), "update_report_scheduler").ServeHTTP)
r.Get("/{reportID}/template", otelhttp.NewHandler(kithttp.NewServer(
viewReportTemplateEndpoint(svc),
decodeGetReportTemplateRequest,
api.EncodeResponse,
opts...,
), "get_report_template").ServeHTTP)
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
deleteReportConfigEndpoint(svc),
decodeDeleteReportConfigRequest,
api.EncodeResponse,
opts...,
), "delete_report_config").ServeHTTP)
r.Delete("/{reportID}/template", otelhttp.NewHandler(kithttp.NewServer(
deleteReportTemplateEndpoint(svc),
decodeDeleteReportTemplateRequest,
api.EncodeResponse,
opts...,
), "delete_report_template").ServeHTTP)
r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer(
enableReportConfigEndpoint(svc),
decodeUpdateReportStatusRequest,
api.EncodeResponse,
opts...,
), "enable_report_config").ServeHTTP)
r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer(
disableReportConfigEndpoint(svc),
decodeUpdateReportStatusRequest,
api.EncodeResponse,
opts...,
), "disable_report_config").ServeHTTP)
r.Put("/template", otelhttp.NewHandler(kithttp.NewServer(
updateReportTemplateEndpoint(svc),
decodeUpdateReportTemplateRequest,
api.EncodeResponse,
opts...,
), "update_report_template").ServeHTTP)
r.Get("/template", otelhttp.NewHandler(kithttp.NewServer(
viewReportTemplateEndpoint(svc),
decodeGetReportTemplateRequest,
api.EncodeResponse,
opts...,
), "get_report_template").ServeHTTP)
r.Delete("/template", otelhttp.NewHandler(kithttp.NewServer(
deleteReportTemplateEndpoint(svc),
decodeDeleteReportTemplateRequest,
api.EncodeResponse,
opts...,
), "delete_report_template").ServeHTTP)
roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts)
})
})
})
})
@@ -170,7 +179,11 @@ func decodeAddReportConfigRequest(_ context.Context, r *http.Request) (any, erro
func decodeViewReportConfigRequest(_ context.Context, r *http.Request) (any, error) {
id := chi.URLParam(r, reportIdKey)
return viewReportConfigReq{ID: id}, nil
withRoles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false)
if err != nil {
return nil, err
}
return viewReportConfigReq{ID: id, withRoles: withRoles}, nil
}
func decodeUpdateReportConfigRequest(_ context.Context, r *http.Request) (any, error) {
+8
View File
@@ -0,0 +1,8 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import "github.com/absmach/supermq/pkg/roles"
const BuiltInRoleAdmin roles.BuiltInRoleName = "admin"
+40 -27
View File
@@ -7,11 +7,13 @@ import (
"context"
"github.com/absmach/magistrala/reports"
"github.com/absmach/magistrala/reports/operations"
"github.com/absmach/supermq/pkg/authn"
smqauthz "github.com/absmach/supermq/pkg/authz"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/permissions"
"github.com/absmach/supermq/pkg/policies"
rolemgr "github.com/absmach/supermq/pkg/roles/rolemanager/middleware"
)
var (
@@ -27,36 +29,47 @@ var (
)
type authorizationMiddleware struct {
svc reports.Service
authz smqauthz.Authorization
svc reports.Service
authz smqauthz.Authorization
entitiesOps permissions.EntitiesOperations[permissions.Operation]
rolemgr.RoleManagerAuthorizationMiddleware
}
// AuthorizationMiddleware adds authorization to the reports service.
func AuthorizationMiddleware(svc reports.Service, authz smqauthz.Authorization) (reports.Service, error) {
func AuthorizationMiddleware(svc reports.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation], roleOps permissions.Operations[permissions.RoleOperation]) (reports.Service, error) {
if err := entitiesOps.Validate(); err != nil {
return nil, err
}
ram, err := rolemgr.NewAuthorization(operations.EntityType, svc, authz, roleOps)
if err != nil {
return nil, err
}
return &authorizationMiddleware{
svc: svc,
authz: authz,
svc: svc,
authz: authz,
entitiesOps: entitiesOps,
RoleManagerAuthorizationMiddleware: ram,
}, nil
}
func (am *authorizationMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
if err := am.authorize(ctx, reports.OpAddReportConfig, session); err != nil {
if err := am.authorize(ctx, operations.OpAddReportConfig, session, policies.DomainType, session.DomainID); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainCreateConfigs, err)
}
return am.svc.AddReportConfig(ctx, session, cfg)
}
func (am *authorizationMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
if err := am.authorize(ctx, reports.OpViewReportConfig, session); err != nil {
func (am *authorizationMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) {
if err := am.authorize(ctx, operations.OpViewReportConfig, session, operations.EntityType, id); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainViewConfigs, err)
}
return am.svc.ViewReportConfig(ctx, session, id)
return am.svc.ViewReportConfig(ctx, session, id, withRoles)
}
func (am *authorizationMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
if err := am.authorize(ctx, reports.OpUpdateReportConfig, session); err != nil {
if err := am.authorize(ctx, operations.OpUpdateReportConfig, session, operations.EntityType, cfg.ID); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
@@ -64,15 +77,15 @@ func (am *authorizationMiddleware) UpdateReportConfig(ctx context.Context, sessi
}
func (am *authorizationMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
if err := am.authorize(ctx, reports.OpUpdateReportSchedule, session); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainDeleteConfigs, err)
if err := am.authorize(ctx, operations.OpUpdateReportSchedule, session, operations.EntityType, cfg.ID); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
return am.svc.UpdateReportSchedule(ctx, session, cfg)
}
func (am *authorizationMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
if err := am.authorize(ctx, reports.OpRemoveReportConfig, session); err != nil {
if err := am.authorize(ctx, operations.OpRemoveReportConfig, session, operations.EntityType, id); err != nil {
return errors.Wrap(errDomainDeleteConfigs, err)
}
@@ -80,7 +93,7 @@ func (am *authorizationMiddleware) RemoveReportConfig(ctx context.Context, sessi
}
func (am *authorizationMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) {
if err := am.authorize(ctx, reports.OpListReportsConfig, session); err != nil {
if err := am.authorize(ctx, operations.OpListReportsConfig, session, policies.DomainType, session.DomainID); err != nil {
return reports.ReportConfigPage{}, errors.Wrap(errDomainViewConfigs, err)
}
@@ -88,7 +101,7 @@ func (am *authorizationMiddleware) ListReportsConfig(ctx context.Context, sessio
}
func (am *authorizationMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
if err := am.authorize(ctx, reports.OpEnableReportConfig, session); err != nil {
if err := am.authorize(ctx, operations.OpEnableReportConfig, session, operations.EntityType, id); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
@@ -96,7 +109,7 @@ func (am *authorizationMiddleware) EnableReportConfig(ctx context.Context, sessi
}
func (am *authorizationMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
if err := am.authorize(ctx, reports.OpDisableReportConfig, session); err != nil {
if err := am.authorize(ctx, operations.OpDisableReportConfig, session, operations.EntityType, id); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
@@ -104,7 +117,7 @@ func (am *authorizationMiddleware) DisableReportConfig(ctx context.Context, sess
}
func (am *authorizationMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) {
if err := am.authorize(ctx, reports.OpGenerateReport, session); err != nil {
if err := am.authorize(ctx, operations.OpGenerateReport, session, policies.DomainType, session.DomainID); err != nil {
return reports.ReportPage{}, errors.Wrap(errDomainGenerateReports, err)
}
@@ -112,7 +125,7 @@ func (am *authorizationMiddleware) GenerateReport(ctx context.Context, session a
}
func (am *authorizationMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error {
if err := am.authorize(ctx, reports.OpUpdateReportTemplate, session); err != nil {
if err := am.authorize(ctx, operations.OpUpdateReportTemplate, session, operations.EntityType, cfg.ID); err != nil {
return errors.Wrap(errDomainUpdateTemplates, err)
}
@@ -120,7 +133,7 @@ func (am *authorizationMiddleware) UpdateReportTemplate(ctx context.Context, ses
}
func (am *authorizationMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) {
if err := am.authorize(ctx, reports.OpViewReportTemplate, session); err != nil {
if err := am.authorize(ctx, operations.OpViewReportTemplate, session, operations.EntityType, id); err != nil {
return "", errors.Wrap(errDomainViewTemplates, err)
}
@@ -128,7 +141,7 @@ func (am *authorizationMiddleware) ViewReportTemplate(ctx context.Context, sessi
}
func (am *authorizationMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error {
if err := am.authorize(ctx, reports.OpDeleteReportTemplate, session); err != nil {
if err := am.authorize(ctx, operations.OpDeleteReportTemplate, session, operations.EntityType, id); err != nil {
return errors.Wrap(errDomainRemoveTemplates, err)
}
@@ -139,8 +152,8 @@ func (am *authorizationMiddleware) StartScheduler(ctx context.Context) error {
return am.svc.StartScheduler(ctx)
}
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session) error {
perm, err := reports.GetPermission(op)
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error {
perm, err := am.entitiesOps.GetPermission(operations.EntityType, op)
if err != nil {
return err
}
@@ -150,19 +163,19 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: perm,
Object: obj,
ObjectType: objType,
Permission: perm.String(),
}
var pat *smqauthz.PATReq
if session.PatID != "" {
opName := reports.OperationName(op)
opName := am.entitiesOps.OperationName(operations.EntityType, op)
pat = &smqauthz.PATReq{
UserID: session.UserID,
PatID: session.PatID,
EntityID: session.DomainID,
EntityType: reports.EntityType,
EntityType: operations.EntityType,
Operation: opName,
Domain: session.DomainID,
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"time"
mgPolicies "github.com/absmach/magistrala/pkg/policies"
"github.com/absmach/magistrala/reports"
"github.com/absmach/magistrala/reports/operations"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/callout"
"github.com/absmach/supermq/pkg/permissions"
"github.com/absmach/supermq/pkg/policies"
rolemw "github.com/absmach/supermq/pkg/roles/rolemanager/middleware"
)
var _ reports.Service = (*calloutMiddleware)(nil)
type calloutMiddleware struct {
svc reports.Service
callout callout.Callout
entitiesOps permissions.EntitiesOperations[permissions.Operation]
rolemw.RoleManagerCalloutMiddleware
}
const entityType = "report"
func NewCallout(svc reports.Service, callout callout.Callout, entitiesOps permissions.EntitiesOperations[permissions.Operation], roleOps permissions.Operations[permissions.RoleOperation]) (reports.Service, error) {
call, err := rolemw.NewCallout(mgPolicies.ReportsType, svc, callout, roleOps)
if err != nil {
return nil, err
}
if err := entitiesOps.Validate(); err != nil {
return nil, err
}
return &calloutMiddleware{
svc: svc,
callout: callout,
entitiesOps: entitiesOps,
RoleManagerCalloutMiddleware: call,
}, nil
}
func (cm *calloutMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
params := map[string]any{
"entities": cfg,
"count": 1,
}
if err := cm.callOut(ctx, session, operations.OpAddReportConfig, params); err != nil {
return reports.ReportConfig{}, err
}
return cm.svc.AddReportConfig(ctx, session, cfg)
}
func (cm *calloutMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) {
params := map[string]any{
"entity_id": id,
}
if err := cm.callOut(ctx, session, operations.OpViewReportConfig, params); err != nil {
return reports.ReportConfig{}, err
}
return cm.svc.ViewReportConfig(ctx, session, id, withRoles)
}
func (cm *calloutMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
params := map[string]any{
"entity_id": cfg.ID,
}
if err := cm.callOut(ctx, session, operations.OpUpdateReportConfig, params); err != nil {
return reports.ReportConfig{}, err
}
return cm.svc.UpdateReportConfig(ctx, session, cfg)
}
func (cm *calloutMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
params := map[string]any{
"entity_id": cfg.ID,
}
if err := cm.callOut(ctx, session, operations.OpUpdateReportSchedule, params); err != nil {
return reports.ReportConfig{}, err
}
return cm.svc.UpdateReportSchedule(ctx, session, cfg)
}
func (cm *calloutMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
params := map[string]any{
"entity_id": id,
}
if err := cm.callOut(ctx, session, operations.OpRemoveReportConfig, params); err != nil {
return err
}
return cm.svc.RemoveReportConfig(ctx, session, id)
}
func (cm *calloutMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) {
params := map[string]any{
"pagemeta": pm,
}
if err := cm.callOut(ctx, session, operations.OpListReportsConfig, params); err != nil {
return reports.ReportConfigPage{}, err
}
return cm.svc.ListReportsConfig(ctx, session, pm)
}
func (cm *calloutMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
params := map[string]any{
"entity_id": id,
}
if err := cm.callOut(ctx, session, operations.OpEnableReportConfig, params); err != nil {
return reports.ReportConfig{}, err
}
return cm.svc.EnableReportConfig(ctx, session, id)
}
func (cm *calloutMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
params := map[string]any{
"entity_id": id,
}
if err := cm.callOut(ctx, session, operations.OpDisableReportConfig, params); err != nil {
return reports.ReportConfig{}, err
}
return cm.svc.DisableReportConfig(ctx, session, id)
}
func (cm *calloutMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) {
params := map[string]any{
"entity_id": config.ID,
}
if err := cm.callOut(ctx, session, operations.OpGenerateReport, params); err != nil {
return reports.ReportPage{}, err
}
return cm.svc.GenerateReport(ctx, session, config, action)
}
func (cm *calloutMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error {
params := map[string]any{
"entity_id": cfg.ID,
}
if err := cm.callOut(ctx, session, operations.OpUpdateReportTemplate, params); err != nil {
return err
}
return cm.svc.UpdateReportTemplate(ctx, session, cfg)
}
func (cm *calloutMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) {
params := map[string]any{
"entity_id": id,
}
if err := cm.callOut(ctx, session, operations.OpViewReportTemplate, params); err != nil {
return "", err
}
return cm.svc.ViewReportTemplate(ctx, session, id)
}
func (cm *calloutMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error {
params := map[string]any{
"entity_id": id,
}
if err := cm.callOut(ctx, session, operations.OpDeleteReportTemplate, params); err != nil {
return err
}
return cm.svc.DeleteReportTemplate(ctx, session, id)
}
func (cm *calloutMiddleware) StartScheduler(ctx context.Context) error {
return cm.svc.StartScheduler(ctx)
}
func (cm *calloutMiddleware) callOut(ctx context.Context, session authn.Session, op permissions.Operation, pld map[string]any) error {
var entityID string
if id, ok := pld["entity_id"].(string); ok {
entityID = id
}
req := callout.Request{
BaseRequest: callout.BaseRequest{
Operation: cm.entitiesOps.OperationName(entityType, op),
EntityType: entityType,
EntityID: entityID,
CallerID: session.UserID,
CallerType: policies.UserType,
DomainID: session.DomainID,
Time: time.Now().UTC(),
},
Payload: pld,
}
if err := cm.callout.Callout(ctx, req); err != nil {
return err
}
return nil
}
+9 -3
View File
@@ -10,6 +10,7 @@ import (
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/authn"
rolemw "github.com/absmach/supermq/pkg/roles/rolemanager/middleware"
)
var _ reports.Service = (*loggingMiddleware)(nil)
@@ -17,10 +18,15 @@ var _ reports.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger *slog.Logger
svc reports.Service
rolemw.RoleManagerLoggingMiddleware
}
func LoggingMiddleware(svc reports.Service, logger *slog.Logger) reports.Service {
return &loggingMiddleware{logger, svc}
return &loggingMiddleware{
logger: logger,
svc: svc,
RoleManagerLoggingMiddleware: rolemw.NewLogging("reports", svc, logger),
}
}
func (lm *loggingMiddleware) StartScheduler(ctx context.Context) (err error) {
@@ -71,7 +77,7 @@ func (lm *loggingMiddleware) AddReportConfig(ctx context.Context, session authn.
return lm.svc.AddReportConfig(ctx, session, config)
}
func (lm *loggingMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string) (res reports.ReportConfig, err error) {
func (lm *loggingMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
@@ -88,7 +94,7 @@ func (lm *loggingMiddleware) ViewReportConfig(ctx context.Context, session authn
}
lm.logger.Info("View report config completed successfully", args...)
}(time.Now())
return lm.svc.ViewReportConfig(ctx, session, id)
return lm.svc.ViewReportConfig(ctx, session, id, withRoles)
}
func (lm *loggingMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, config reports.ReportConfig) (res reports.ReportConfig, err error) {
+149
View File
@@ -0,0 +1,149 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"time"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/authn"
rolemw "github.com/absmach/supermq/pkg/roles/rolemanager/middleware"
"github.com/go-kit/kit/metrics"
)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
service reports.Service
rolemw.RoleManagerMetricsMiddleware
}
var _ reports.Service = (*metricsMiddleware)(nil)
func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, service reports.Service) reports.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
service: service,
RoleManagerMetricsMiddleware: rolemw.NewMetrics("reports", service, counter, latency),
}
}
func (mm *metricsMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
defer func(begin time.Time) {
mm.counter.With("method", "add_report_config").Add(1)
mm.latency.With("method", "add_report_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.AddReportConfig(ctx, session, cfg)
}
func (mm *metricsMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) {
defer func(begin time.Time) {
mm.counter.With("method", "view_report_config").Add(1)
mm.latency.With("method", "view_report_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.ViewReportConfig(ctx, session, id, withRoles)
}
func (mm *metricsMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_report_config").Add(1)
mm.latency.With("method", "update_report_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.UpdateReportConfig(ctx, session, cfg)
}
func (mm *metricsMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_report_schedule").Add(1)
mm.latency.With("method", "update_report_schedule").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.UpdateReportSchedule(ctx, session, cfg)
}
func (mm *metricsMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
defer func(begin time.Time) {
mm.counter.With("method", "remove_report_config").Add(1)
mm.latency.With("method", "remove_report_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.RemoveReportConfig(ctx, session, id)
}
func (mm *metricsMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) {
defer func(begin time.Time) {
mm.counter.With("method", "list_reports_config").Add(1)
mm.latency.With("method", "list_reports_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.ListReportsConfig(ctx, session, pm)
}
func (mm *metricsMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
defer func(begin time.Time) {
mm.counter.With("method", "enable_report_config").Add(1)
mm.latency.With("method", "enable_report_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.EnableReportConfig(ctx, session, id)
}
func (mm *metricsMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
defer func(begin time.Time) {
mm.counter.With("method", "disable_report_config").Add(1)
mm.latency.With("method", "disable_report_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.DisableReportConfig(ctx, session, id)
}
func (mm *metricsMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error {
defer func(begin time.Time) {
mm.counter.With("method", "update_report_template").Add(1)
mm.latency.With("method", "update_report_template").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.UpdateReportTemplate(ctx, session, cfg)
}
func (mm *metricsMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) {
defer func(begin time.Time) {
mm.counter.With("method", "view_report_template").Add(1)
mm.latency.With("method", "view_report_template").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.ViewReportTemplate(ctx, session, id)
}
func (mm *metricsMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error {
defer func(begin time.Time) {
mm.counter.With("method", "delete_report_template").Add(1)
mm.latency.With("method", "delete_report_template").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.DeleteReportTemplate(ctx, session, id)
}
func (mm *metricsMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) {
defer func(begin time.Time) {
mm.counter.With("method", "generate_report").Add(1)
mm.latency.With("method", "generate_report").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.GenerateReport(ctx, session, config, action)
}
func (mm *metricsMiddleware) StartScheduler(ctx context.Context) error {
defer func(begin time.Time) {
mm.counter.With("method", "start_scheduler").Add(1)
mm.latency.With("method", "start_scheduler").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.service.StartScheduler(ctx)
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/authn"
rolemw "github.com/absmach/supermq/pkg/roles/rolemanager/middleware"
smqTracing "github.com/absmach/supermq/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type tracingMiddleware struct {
tracer trace.Tracer
svc reports.Service
rolemw.RoleManagerTracing
}
var _ reports.Service = (*tracingMiddleware)(nil)
func NewTracingMiddleware(tracer trace.Tracer, svc reports.Service) reports.Service {
return &tracingMiddleware{
tracer: tracer,
svc: svc,
RoleManagerTracing: rolemw.NewTracing("reports", svc, tracer),
}
}
func (tm *tracingMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "add_report_config", trace.WithAttributes(
attribute.String("name", cfg.Name),
attribute.String("domain_id", cfg.DomainID),
))
defer span.End()
return tm.svc.AddReportConfig(ctx, session, cfg)
}
func (tm *tracingMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (reports.ReportConfig, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "view_report_config", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.ViewReportConfig(ctx, session, id, withRoles)
}
func (tm *tracingMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_report_config", trace.WithAttributes(
attribute.String("id", cfg.ID),
))
defer span.End()
return tm.svc.UpdateReportConfig(ctx, session, cfg)
}
func (tm *tracingMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_report_schedule", trace.WithAttributes(
attribute.String("id", cfg.ID),
))
defer span.End()
return tm.svc.UpdateReportSchedule(ctx, session, cfg)
}
func (tm *tracingMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "remove_report_config", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.RemoveReportConfig(ctx, session, id)
}
func (tm *tracingMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "list_reports_config", trace.WithAttributes(
attribute.Int("offset", int(pm.Offset)),
attribute.Int("limit", int(pm.Limit)),
))
defer span.End()
return tm.svc.ListReportsConfig(ctx, session, pm)
}
func (tm *tracingMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "enable_report_config", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.EnableReportConfig(ctx, session, id)
}
func (tm *tracingMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "disable_report_config", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.DisableReportConfig(ctx, session, id)
}
func (tm *tracingMiddleware) UpdateReportTemplate(ctx context.Context, session authn.Session, cfg reports.ReportConfig) error {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_report_template", trace.WithAttributes(
attribute.String("id", cfg.ID),
))
defer span.End()
return tm.svc.UpdateReportTemplate(ctx, session, cfg)
}
func (tm *tracingMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "view_report_template", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.ViewReportTemplate(ctx, session, id)
}
func (tm *tracingMiddleware) DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "delete_report_template", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.DeleteReportTemplate(ctx, session, id)
}
func (tm *tracingMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "generate_report", trace.WithAttributes(
attribute.String("config_id", config.ID),
attribute.String("action", string(action)),
))
defer span.End()
return tm.svc.GenerateReport(ctx, session, config, action)
}
func (tm *tracingMiddleware) StartScheduler(ctx context.Context) error {
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "start_scheduler")
defer span.End()
return tm.svc.StartScheduler(ctx)
}
File diff suppressed because it is too large Load Diff
+1500 -12
View File
File diff suppressed because it is too large Load Diff
-80
View File
@@ -1,80 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/permissions"
"github.com/absmach/supermq/pkg/policies"
)
const EntityType = "reports"
const (
OpAddReportConfig = iota
OpViewReportConfig
OpUpdateReportConfig
OpUpdateReportSchedule
OpRemoveReportConfig
OpListReportsConfig
OpEnableReportConfig
OpDisableReportConfig
OpGenerateReport
OpUpdateReportTemplate
OpViewReportTemplate
OpDeleteReportTemplate
)
const (
OpAddReportConfigStr = "OpAddReportConfig"
OpViewReportConfigStr = "OpViewReportConfig"
OpUpdateReportConfigStr = "OpUpdateReportConfig"
OpUpdateReportScheduleStr = "OpUpdateReportSchedule"
OpRemoveReportConfigStr = "OpRemoveReportConfig"
OpListReportsConfigStr = "OpListReportsConfig"
OpEnableReportConfigStr = "OpEnableReportConfig"
OpDisableReportConfigStr = "OpDisableReportConfig"
OpGenerateReportStr = "OpGenerateReport"
OpUpdateReportTemplateStr = "OpUpdateReportTemplate"
OpViewReportTemplateStr = "OpViewReportTemplate"
OpDeleteReportTemplateStr = "OpDeleteReportTemplate"
)
func GetPermission(op permissions.Operation) (string, error) {
if op < OpAddReportConfig || op > OpDeleteReportTemplate {
return "", errors.New("invalid operation")
}
return policies.MembershipPermission, nil
}
func OperationName(op permissions.Operation) string {
switch op {
case OpAddReportConfig:
return OpAddReportConfigStr
case OpViewReportConfig:
return OpViewReportConfigStr
case OpUpdateReportConfig:
return OpUpdateReportConfigStr
case OpUpdateReportSchedule:
return OpUpdateReportScheduleStr
case OpRemoveReportConfig:
return OpRemoveReportConfigStr
case OpListReportsConfig:
return OpListReportsConfigStr
case OpEnableReportConfig:
return OpEnableReportConfigStr
case OpDisableReportConfig:
return OpDisableReportConfigStr
case OpGenerateReport:
return OpGenerateReportStr
case OpUpdateReportTemplate:
return OpUpdateReportTemplateStr
case OpViewReportTemplate:
return OpViewReportTemplateStr
case OpDeleteReportTemplate:
return OpDeleteReportTemplateStr
default:
return "unknown"
}
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package operations
import "github.com/absmach/supermq/pkg/permissions"
const EntityType = "report"
// Report Operations.
const (
OpAddReportConfig permissions.Operation = iota
OpViewReportConfig
OpUpdateReportConfig
OpUpdateReportSchedule
OpRemoveReportConfig
OpListReportsConfig
OpEnableReportConfig
OpDisableReportConfig
OpGenerateReport
OpUpdateReportTemplate
OpViewReportTemplate
OpDeleteReportTemplate
)
func OperationDetails() map[permissions.Operation]permissions.OperationDetails {
return map[permissions.Operation]permissions.OperationDetails{
OpAddReportConfig: {
Name: "add",
PermissionRequired: true,
},
OpViewReportConfig: {
Name: "view",
PermissionRequired: true,
},
OpUpdateReportConfig: {
Name: "update",
PermissionRequired: true,
},
OpUpdateReportSchedule: {
Name: "update_schedule",
PermissionRequired: true,
},
OpRemoveReportConfig: {
Name: "delete",
PermissionRequired: true,
},
OpListReportsConfig: {
Name: "list",
PermissionRequired: true,
},
OpEnableReportConfig: {
Name: "enable",
PermissionRequired: true,
},
OpDisableReportConfig: {
Name: "disable",
PermissionRequired: true,
},
OpGenerateReport: {
Name: "generate",
PermissionRequired: true,
},
OpUpdateReportTemplate: {
Name: "update_template",
PermissionRequired: true,
},
OpViewReportTemplate: {
Name: "view_template",
PermissionRequired: true,
},
OpDeleteReportTemplate: {
Name: "delete_template",
PermissionRequired: true,
},
}
}
+20 -2
View File
@@ -4,12 +4,20 @@
package postgres
import (
dpostgres "github.com/absmach/supermq/domains/postgres"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
rolesPostgres "github.com/absmach/supermq/pkg/roles/repo/postgres"
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
migrate "github.com/rubenv/sql-migrate"
)
func Migration() *migrate.MemoryMigrationSource {
return &migrate.MemoryMigrationSource{
func Migration() (*migrate.MemoryMigrationSource, error) {
rolesMigration, err := rolesPostgres.Migration(rolesTableNamePrefix, entityTableName, entityIDColumnName)
if err != nil {
return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err)
}
reportsMigration := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "reports_01",
@@ -48,4 +56,14 @@ func Migration() *migrate.MemoryMigrationSource {
},
},
}
reportsMigration.Migrations = append(reportsMigration.Migrations, rolesMigration.Migrations...)
domainsMigration, err := dpostgres.Migration()
if err != nil {
return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err)
}
reportsMigration.Migrations = append(reportsMigration.Migrations, domainsMigration.Migrations...)
return reportsMigration, nil
}
+11
View File
@@ -11,6 +11,7 @@ import (
"github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/roles"
)
// dbReport represents the database structure for a Report.
@@ -32,6 +33,8 @@ type dbReport struct {
Metrics []byte `db:"metrics"`
Email []byte `db:"email"`
ReportTemplate reports.ReportTemplate `db:"report_template"`
MemberID string `db:"member_id,omitempty"`
Roles json.RawMessage `db:"roles,omitempty"`
}
func reportToDb(r reports.ReportConfig) (dbReport, error) {
@@ -113,6 +116,13 @@ func dbToReport(dto dbReport) (reports.ReportConfig, error) {
}
}
var roles []roles.MemberRoleActions
if dto.Roles != nil {
if err := json.Unmarshal(dto.Roles, &roles); err != nil {
return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
}
rpt := reports.ReportConfig{
ID: dto.ID,
Name: dto.Name,
@@ -133,6 +143,7 @@ func dbToReport(dto dbReport) (reports.ReportConfig, error) {
UpdatedAt: dto.UpdatedAt,
UpdatedBy: dto.UpdatedBy,
ReportTemplate: dto.ReportTemplate,
Roles: roles,
}
return rpt, nil
+162 -2
View File
@@ -10,25 +10,36 @@ import (
"strings"
"time"
mgPolicies "github.com/absmach/magistrala/pkg/policies"
"github.com/absmach/magistrala/reports"
api "github.com/absmach/supermq/api/http"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
"github.com/absmach/supermq/pkg/postgres"
rolesPostgres "github.com/absmach/supermq/pkg/roles/repo/postgres"
)
const (
rolesTableNamePrefix = "reports"
entityTableName = "report_config"
entityIDColumnName = "id"
)
type PostgresRepository struct {
DB postgres.Database
eh errors.Handler
rolesPostgres.Repository
}
func NewRepository(db postgres.Database) reports.Repository {
rolesRepo := rolesPostgres.NewRepository(db, mgPolicies.ReportsType, rolesTableNamePrefix, entityTableName, entityIDColumnName)
errHandlerOptions := []errors.HandlerOption{
postgres.WithDuplicateErrors(NewDuplicateErrors()),
}
return &PostgresRepository{
DB: db,
eh: postgres.NewErrorHandler(errHandlerOptions...),
DB: db,
eh: postgres.NewErrorHandler(errHandlerOptions...),
Repository: rolesRepo,
}
}
@@ -92,6 +103,155 @@ func (repo *PostgresRepository) ViewReportConfig(ctx context.Context, id string)
return rpt, nil
}
func (repo *PostgresRepository) RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (reports.ReportConfig, error) {
query := `
WITH selected_report AS (
SELECT
r.id,
r.domain_id
FROM
report_config r
WHERE
r.id = :id
LIMIT 1
),
selected_report_roles AS (
SELECT
rr.entity_id AS report_id,
rrm.member_id AS member_id,
rr.id AS role_id,
rr."name" AS role_name,
jsonb_agg(DISTINCT rra."action") AS actions,
'direct' AS access_type,
'' AS access_provider_id
FROM
reports_roles rr
JOIN
reports_role_members rrm ON rr.id = rrm.role_id
JOIN
reports_role_actions rra ON rr.id = rra.role_id
JOIN
selected_report sr ON sr.id = rr.entity_id
AND rrm.member_id = :member_id
GROUP BY
rr.entity_id, rr.id, rr.name, rrm.member_id
),
selected_domain_roles AS (
SELECT
sr.id AS report_id,
drm.member_id AS member_id,
dr.id AS role_id,
dr."name" AS role_name,
jsonb_agg(DISTINCT all_actions."action") AS actions,
'domain' AS access_type,
dr.entity_id AS access_provider_id
FROM
domains d
JOIN
selected_report sr ON sr.domain_id = d.id
JOIN
domains_roles dr ON dr.entity_id = d.id
JOIN
domains_role_members drm ON dr.id = drm.role_id
JOIN
domains_role_actions dra ON dr.id = dra.role_id
JOIN
domains_role_actions all_actions ON dr.id = all_actions.role_id
WHERE
drm.member_id = :member_id
AND dra."action" LIKE 'report%'
GROUP BY
sr.id, dr.entity_id, dr.id, dr."name", drm.member_id
),
all_roles AS (
SELECT
srr.report_id,
srr.member_id,
srr.role_id,
srr.role_name,
srr.actions,
srr.access_type,
srr.access_provider_id
FROM
selected_report_roles srr
UNION
SELECT
sdr.report_id,
sdr.member_id,
sdr.role_id,
sdr.role_name,
sdr.actions,
sdr.access_type,
sdr.access_provider_id
FROM
selected_domain_roles sdr
),
final_roles AS (
SELECT
ar.report_id,
ar.member_id,
jsonb_agg(
jsonb_build_object(
'role_id', ar.role_id,
'role_name', ar.role_name,
'actions', ar.actions,
'access_type', ar.access_type,
'access_provider_id', ar.access_provider_id
)
) AS roles
FROM all_roles ar
GROUP BY
ar.report_id, ar.member_id
)
SELECT
r2.id,
r2."name",
r2.description,
r2.domain_id,
r2.status,
r2.created_at,
r2.created_by,
r2.updated_at,
r2.updated_by,
r2.due,
r2.recurring,
r2.recurring_period,
r2.start_datetime,
r2.config,
r2.email,
r2.metrics,
r2.report_template,
fr.member_id,
fr.roles
FROM report_config r2
JOIN final_roles fr ON fr.report_id = r2.id
`
parameters := map[string]any{
"id": id,
"member_id": memberID,
}
row, err := repo.DB.NamedQueryContext(ctx, query, parameters)
if err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer row.Close()
dbreport := dbReport{}
if !row.Next() {
return reports.ReportConfig{}, repoerr.ErrNotFound
}
if err := row.StructScan(&dbreport); err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
cfg, err := dbToReport(dbreport)
if err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return cfg, nil
}
func (repo *PostgresRepository) UpdateReportConfigStatus(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
q := `UPDATE report_config SET status = :status, updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id
+5 -1
View File
@@ -74,7 +74,11 @@ func TestMain(m *testing.M) {
SSLRootCert: "",
}
if db, err = postgres.Setup(dbConfig, *rpostgres.Migration()); err != nil {
migration, err := rpostgres.Migration()
if err != nil {
log.Fatalf("Could not get migration: %s", err)
}
if db, err = postgres.Setup(dbConfig, *migration); err != nil {
log.Fatalf("Could not setup test DB connection: %s", err)
}
+20 -15
View File
@@ -15,6 +15,7 @@ import (
"github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/roles"
"github.com/absmach/supermq/pkg/transformers/senml"
)
@@ -152,20 +153,21 @@ func (rm ReqMetric) Validate() error {
}
type ReportConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
DomainID string `json:"domain_id"`
Schedule schedule.Schedule `json:"schedule,omitempty"`
Config *MetricConfig `json:"config,omitempty"`
Email *EmailSetting `json:"email,omitempty"`
Metrics []ReqMetric `json:"metrics,omitempty"`
ReportTemplate ReportTemplate `json:"report_template,omitempty"`
Status Status `json:"status"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedBy string `json:"updated_by,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
DomainID string `json:"domain_id"`
Schedule schedule.Schedule `json:"schedule,omitempty"`
Config *MetricConfig `json:"config,omitempty"`
Email *EmailSetting `json:"email,omitempty"`
Metrics []ReqMetric `json:"metrics,omitempty"`
ReportTemplate ReportTemplate `json:"report_template,omitempty"`
Status Status `json:"status"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedBy string `json:"updated_by,omitempty"`
Roles []roles.MemberRoleActions `json:"roles,omitempty"`
}
type ReportConfigPage struct {
@@ -398,6 +400,7 @@ type PageMeta struct {
type Repository interface {
AddReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
ViewReportConfig(ctx context.Context, id string) (ReportConfig, error)
RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (ReportConfig, error)
UpdateReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
UpdateReportSchedule(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
RemoveReportConfig(ctx context.Context, id string) error
@@ -408,11 +411,12 @@ type Repository interface {
UpdateReportTemplate(ctx context.Context, domainID, reportID string, template ReportTemplate) error
ViewReportTemplate(ctx context.Context, domainID, reportID string) (ReportTemplate, error)
DeleteReportTemplate(ctx context.Context, domainID, reportID string) error
roles.Repository
}
type Service interface {
AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
ViewReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error)
ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (ReportConfig, error)
UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
UpdateReportSchedule(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
RemoveReportConfig(ctx context.Context, session authn.Session, id string) error
@@ -426,4 +430,5 @@ type Service interface {
GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error)
StartScheduler(ctx context.Context) error
roles.RoleManager
}
+56 -13
View File
@@ -15,10 +15,13 @@ import (
pkglog "github.com/absmach/magistrala/pkg/logger"
"github.com/absmach/magistrala/pkg/reltime"
"github.com/absmach/magistrala/pkg/ticker"
"github.com/absmach/magistrala/reports/operations"
"github.com/absmach/supermq"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/policies"
"github.com/absmach/supermq/pkg/roles"
"github.com/absmach/supermq/pkg/transformers/senml"
)
@@ -33,22 +36,28 @@ type report struct {
readers grpcReadersV1.ReadersServiceClient
defaultTemplate ReportTemplate
converterURL string
roles.ProvisionManageService
}
func NewService(repo Repository, runInfo chan pkglog.RunInfo, idp supermq.IDProvider, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient, template ReportTemplate, converterURL string) Service {
return &report{
repo: repo,
idp: idp,
runInfo: runInfo,
email: emailer,
ticker: tck,
readers: readers,
defaultTemplate: template,
converterURL: converterURL,
func NewService(repo Repository, runInfo chan pkglog.RunInfo, policy policies.Service, idp supermq.IDProvider, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient, template ReportTemplate, converterURL string, availableActions []roles.Action, builtInRoles map[roles.BuiltInRoleName][]roles.Action) (Service, error) {
rpms, err := roles.NewProvisionManageService(operations.EntityType, repo, policy, idp, availableActions, builtInRoles)
if err != nil {
return nil, err
}
return &report{
repo: repo,
idp: idp,
runInfo: runInfo,
email: emailer,
ticker: tck,
readers: readers,
defaultTemplate: template,
converterURL: converterURL,
ProvisionManageService: rpms,
}, nil
}
func (r *report) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) {
func (r *report) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (retCfg ReportConfig, retErr error) {
id, err := r.idp.ID()
if err != nil {
return ReportConfig{}, err
@@ -71,11 +80,45 @@ func (r *report) AddReportConfig(ctx context.Context, session authn.Session, cfg
return ReportConfig{}, errors.Wrap(svcerr.ErrCreateEntity, err)
}
defer func() {
if retErr != nil {
if errRollBack := r.repo.RemoveReportConfig(ctx, reportConfig.ID); errRollBack != nil {
retErr = errors.Wrap(retErr, errors.Wrap(svcerr.ErrRollbackRepo, errRollBack))
}
}
}()
newBuiltInRoleMembers := map[roles.BuiltInRoleName][]roles.Member{
BuiltInRoleAdmin: {roles.Member(session.UserID)},
}
optionalPolicies := []policies.Policy{
{
SubjectType: policies.DomainType,
Subject: session.DomainID,
Relation: policies.DomainRelation,
ObjectType: operations.EntityType,
Object: reportConfig.ID,
},
}
_, err = r.AddNewEntitiesRoles(ctx, session.DomainID, session.UserID, []string{reportConfig.ID}, optionalPolicies, newBuiltInRoleMembers)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrAddPolicies, err)
}
return reportConfig, nil
}
func (r *report) ViewReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) {
cfg, err := r.repo.ViewReportConfig(ctx, id)
func (r *report) ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (ReportConfig, error) {
var cfg ReportConfig
var err error
switch withRoles {
case true:
cfg, err = r.repo.RetrieveByIDWithRoles(ctx, id, session.UserID)
default:
cfg, err = r.repo.ViewReportConfig(ctx, id)
}
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
+125 -29
View File
@@ -22,6 +22,8 @@ import (
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
svcerr "github.com/absmach/supermq/pkg/errors/service"
policymocks "github.com/absmach/supermq/pkg/policies/mocks"
"github.com/absmach/supermq/pkg/roles"
"github.com/absmach/supermq/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -52,24 +54,39 @@ var (
}
)
func newService(runInfo chan pkglog.RunInfo) (reports.Service, *mocks.Repository, *tmocks.Ticker) {
func newService(t *testing.T, runInfo chan pkglog.RunInfo) (reports.Service, *mocks.Repository, *tmocks.Ticker, *policymocks.Service) {
repo := new(mocks.Repository)
mockTicker := new(tmocks.Ticker)
idProvider := uuid.NewMock()
readersSvc := new(readmocks.ReadersServiceClient)
e := new(emocks.Emailer)
return reports.NewService(repo, runInfo, idProvider, mockTicker, e, readersSvc, template, ""), repo, mockTicker
policy := new(policymocks.Service)
availableActions := []roles.Action{}
builtInRoles := map[roles.BuiltInRoleName][]roles.Action{
"admin": availableActions,
}
svc, err := reports.NewService(repo, runInfo, policy, idProvider, mockTicker, e, readersSvc, template, "", availableActions, builtInRoles)
if err != nil {
t.Fatalf("Failed to create service: %v", err)
}
return svc, repo, mockTicker, policy
}
func TestAddReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, policies := newService(t, make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
cfg reports.ReportConfig
res reports.ReportConfig
err error
desc string
session authn.Session
cfg reports.ReportConfig
res reports.ReportConfig
err error
addPoliciesErr error
deletePolicies error
addRoleErr error
deleteErr error
}{
{
desc: "Add report config successfully",
@@ -81,8 +98,11 @@ func TestAddReportConfig(t *testing.T) {
Name: reportName,
Schedule: schedule,
},
res: rptConfig,
err: nil,
res: rptConfig,
err: nil,
addPoliciesErr: nil,
addRoleErr: nil,
deleteErr: nil,
},
{
desc: "Add report config with failed repo",
@@ -94,13 +114,79 @@ func TestAddReportConfig(t *testing.T) {
Name: reportName,
Schedule: schedule,
},
err: repoerr.ErrCreateEntity,
err: repoerr.ErrCreateEntity,
addPoliciesErr: nil,
deletePolicies: nil,
addRoleErr: nil,
deleteErr: nil,
},
{
desc: "Add report config with failed to add policies",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: reportName,
Schedule: schedule,
},
res: rptConfig,
addPoliciesErr: svcerr.ErrAuthorization,
err: svcerr.ErrAddPolicies,
},
{
desc: "Add report config with failed to add policies and failed rollback",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: reportName,
Schedule: schedule,
},
res: rptConfig,
addPoliciesErr: svcerr.ErrAuthorization,
deleteErr: svcerr.ErrRemoveEntity,
err: svcerr.ErrRollbackRepo,
},
{
desc: "Add report config with failed to add roles",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: reportName,
Schedule: schedule,
},
res: rptConfig,
addRoleErr: svcerr.ErrCreateEntity,
err: svcerr.ErrAddPolicies,
},
{
desc: "Add report config with failed to add roles and failed to delete policies",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: reportName,
Schedule: schedule,
},
res: rptConfig,
addRoleErr: svcerr.ErrCreateEntity,
deletePolicies: svcerr.ErrRemoveEntity,
err: svcerr.ErrRemoveEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("AddReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err)
policyCall := policies.On("AddPolicies", context.Background(), mock.Anything).Return(tc.addPoliciesErr)
policyCall2 := policies.On("DeletePolicies", context.Background(), mock.Anything).Return(tc.deletePolicies).Maybe()
repoCall1 := repo.On("AddRoles", context.Background(), mock.Anything).Return([]roles.RoleProvision{}, tc.addRoleErr)
repoCall2 := repo.On("Remove", context.Background(), mock.Anything).Return(tc.deleteErr).Maybe()
res, err := svc.AddReportConfig(context.Background(), tc.session, tc.cfg)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
@@ -108,13 +194,17 @@ func TestAddReportConfig(t *testing.T) {
assert.Equal(t, tc.cfg.Name, res.Name)
assert.Equal(t, tc.cfg.Schedule, res.Schedule)
}
defer repoCall.Unset()
policyCall.Unset()
policyCall2.Unset()
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
})
}
}
func TestViewReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo))
cases := []struct {
desc string
@@ -147,7 +237,7 @@ func TestViewReportConfig(t *testing.T) {
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("ViewReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.ViewReportConfig(context.Background(), tc.session, tc.id)
res, err := svc.ViewReportConfig(context.Background(), tc.session, tc.id, false)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.res, res)
@@ -158,7 +248,7 @@ func TestViewReportConfig(t *testing.T) {
}
func TestUpdateReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo))
newName := namegen.Generate()
now := time.Now().Add(time.Hour)
@@ -220,7 +310,7 @@ func TestUpdateReportConfig(t *testing.T) {
}
func TestListReportsConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo))
numConfigs := 50
now := time.Now().Add(time.Hour)
var configs []reports.ReportConfig
@@ -325,13 +415,14 @@ func TestListReportsConfig(t *testing.T) {
}
func TestRemoveReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, policies := newService(t, make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
id string
err error
desc string
session authn.Session
id string
err error
deletePoliciesErr error
}{
{
desc: "remove report config successfully",
@@ -339,8 +430,9 @@ func TestRemoveReportConfig(t *testing.T) {
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
err: nil,
id: rptConfig.ID,
err: nil,
deletePoliciesErr: nil,
},
{
desc: "remove report config with failed repo",
@@ -348,24 +440,27 @@ func TestRemoveReportConfig(t *testing.T) {
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
err: svcerr.ErrRemoveEntity,
id: rptConfig.ID,
err: svcerr.ErrRemoveEntity,
deletePoliciesErr: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("RemoveReportConfig", mock.Anything, mock.Anything).Return(tc.err)
policyCall := policies.On("DeletePolicies", context.Background(), mock.Anything).Return(tc.deletePoliciesErr)
err := svc.RemoveReportConfig(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
defer repoCall.Unset()
policyCall.Unset()
repoCall.Unset()
})
}
}
func TestEnableReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo))
cases := []struct {
desc string
@@ -413,7 +508,7 @@ func TestEnableReportConfig(t *testing.T) {
}
func TestDisableReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
svc, repo, _, _ := newService(t, make(chan pkglog.RunInfo))
cases := []struct {
desc string
@@ -469,7 +564,8 @@ func TestDisableReportConfig(t *testing.T) {
}
func TestGenerateInstantEmailReport(t *testing.T) {
svc, _, _ := newService(make(chan pkglog.RunInfo))
// nolint:dogsled
svc, _, _, _ := newService(t, make(chan pkglog.RunInfo))
validEmailConfig := reports.EmailSetting{
To: []string{"test@example.com"},