From 954597d241a8c5095ca0d3438e23cf69635313f6 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 27 Feb 2024 17:01:24 +0100 Subject: [PATCH] feat: tenants querying --- internal/command/client/tenant/query.go | 63 ++++++++++++++ internal/command/client/tenant/root.go | 1 + .../datastore/sqlite/tenant_repository.go | 82 +++++++++++++++++++ internal/datastore/tenant_repository.go | 28 +++++++ internal/server/api/mount.go | 1 + internal/server/api/query_agents.go | 20 ++++- internal/server/api/query_tenants.go | 82 +++++++++++++++++++ pkg/client/query_tenants.go | 77 +++++++++++++++++ 8 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 internal/command/client/tenant/query.go create mode 100644 internal/server/api/query_tenants.go create mode 100644 pkg/client/query_tenants.go diff --git a/internal/command/client/tenant/query.go b/internal/command/client/tenant/query.go new file mode 100644 index 0000000..3473c4f --- /dev/null +++ b/internal/command/client/tenant/query.go @@ -0,0 +1,63 @@ +package tenant + +import ( + "os" + + "forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr" + clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag" + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "forge.cadoles.com/Cadoles/emissary/pkg/client" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + "gitlab.com/wpetit/goweb/cli/format" +) + +func QueryCommand() *cli.Command { + return &cli.Command{ + Name: "query", + Usage: "Query tenants", + Flags: clientFlag.ComposeFlags( + &cli.Int64SliceFlag{ + Name: "ids", + Usage: "use `IDS` as query filter", + }, + ), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + options := make([]client.QueryTenantsOptionFunc, 0) + + rawIDs := ctx.StringSlice("ids") + if rawIDs != nil { + tenantIDs := func(ids []string) []datastore.TenantID { + tenantIDs := make([]datastore.TenantID, len(ids)) + for i, id := range ids { + tenantIDs[i] = datastore.TenantID(id) + } + return tenantIDs + }(rawIDs) + options = append(options, client.WithQueryTenantsID(tenantIDs...)) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + tenants, _, err := client.QueryTenants(ctx.Context, options...) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := tenantHints(baseFlags.OutputMode) + + if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(tenants)...); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/client/tenant/root.go b/internal/command/client/tenant/root.go index 45d0f26..7be676f 100644 --- a/internal/command/client/tenant/root.go +++ b/internal/command/client/tenant/root.go @@ -13,6 +13,7 @@ func Root() *cli.Command { GetCommand(), UpdateCommand(), DeleteCommand(), + QueryCommand(), }, } } diff --git a/internal/datastore/sqlite/tenant_repository.go b/internal/datastore/sqlite/tenant_repository.go index 57cef93..bf820fd 100644 --- a/internal/datastore/sqlite/tenant_repository.go +++ b/internal/datastore/sqlite/tenant_repository.go @@ -15,6 +15,88 @@ type TenantRepository struct { repository } +// Query implements datastore.TenantRepository. +func (r *TenantRepository) Query(ctx context.Context, opts ...datastore.TenantQueryOptionFunc) ([]*datastore.Tenant, int, error) { + options := &datastore.TenantQueryOptions{} + for _, fn := range opts { + fn(options) + } + + tenants := make([]*datastore.Tenant, 0) + count := 0 + + err := r.withTxRetry(ctx, func(tx *sql.Tx) error { + query := `SELECT id, label, created_at, updated_at FROM tenants` + + limit := 10 + if options.Limit != nil { + limit = *options.Limit + } + + offset := 0 + if options.Offset != nil { + offset = *options.Offset + } + + filters := "" + paramIndex := 3 + args := []any{offset, limit} + + if options.IDs != nil && len(options.IDs) > 0 { + filter, newArgs, newParamIndex := inFilter("id", paramIndex, options.IDs) + filters += filter + paramIndex = newParamIndex + args = append(args, newArgs...) + } + + if filters != "" { + filters = ` WHERE ` + filters + } + + query += filters + ` LIMIT $2 OFFSET $1` + + logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args)) + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := rows.Close(); err != nil { + err = errors.WithStack(err) + logger.Error(ctx, "could not close rows", logger.CapturedE(err)) + } + }() + + for rows.Next() { + tenant := &datastore.Tenant{} + + if err := rows.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil { + return errors.WithStack(err) + } + + tenants = append(tenants, tenant) + } + + if err := rows.Err(); err != nil { + return errors.WithStack(err) + } + + row := tx.QueryRowContext(ctx, `SELECT count(id) FROM tenants `+filters, args...) + if err := row.Scan(&count); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return nil, 0, errors.WithStack(err) + } + + return tenants, count, nil +} + // Create implements datastore.TenantRepository. func (r *TenantRepository) Create(ctx context.Context, label string) (*datastore.Tenant, error) { var tenant datastore.Tenant diff --git a/internal/datastore/tenant_repository.go b/internal/datastore/tenant_repository.go index 009de67..fd5a346 100644 --- a/internal/datastore/tenant_repository.go +++ b/internal/datastore/tenant_repository.go @@ -7,6 +7,8 @@ type TenantRepository interface { Get(ctx context.Context, id TenantID) (*Tenant, error) Update(ctx context.Context, id TenantID, updates ...TenantUpdateOptionFunc) (*Tenant, error) Delete(ctx context.Context, id TenantID) error + + Query(ctx context.Context, opts ...TenantQueryOptionFunc) ([]*Tenant, int, error) } type TenantUpdateOptionFunc func(*TenantUpdateOptions) @@ -20,3 +22,29 @@ func WithTenantUpdateLabel(label string) TenantUpdateOptionFunc { opts.Label = &label } } + +type TenantQueryOptionFunc func(*TenantQueryOptions) + +type TenantQueryOptions struct { + Limit *int + Offset *int + IDs []TenantID +} + +func WithTenantQueryLimit(limit int) TenantQueryOptionFunc { + return func(opts *TenantQueryOptions) { + opts.Limit = &limit + } +} + +func WithTenantQueryOffset(offset int) TenantQueryOptionFunc { + return func(opts *TenantQueryOptions) { + opts.Offset = &offset + } +} + +func WithTenantQueryID(ids ...TenantID) TenantQueryOptionFunc { + return func(opts *TenantQueryOptions) { + opts.IDs = ids + } +} diff --git a/internal/server/api/mount.go b/internal/server/api/mount.go index 2cf231c..0bfc4eb 100644 --- a/internal/server/api/mount.go +++ b/internal/server/api/mount.go @@ -38,6 +38,7 @@ func (m *Mount) Mount(r chi.Router) { }) r.Route("/tenants", func(r chi.Router) { + r.With(assertQueryAccess).Get("/", m.queryTenants) r.With(assertAdminAccess).Post("/", m.createTenant) r.With(assertAdminOrTenantReadAccess).Get("/{tenantID}", m.getTenant) r.With(assertAdminOrTenantWriteAccess).Put("/{tenantID}", m.updateTenant) diff --git a/internal/server/api/query_agents.go b/internal/server/api/query_agents.go index 63ab72c..24e57de 100644 --- a/internal/server/api/query_agents.go +++ b/internal/server/api/query_agents.go @@ -3,6 +3,7 @@ package api import ( "net/http" + userAuth "forge.cadoles.com/Cadoles/emissary/internal/auth/user" "forge.cadoles.com/Cadoles/emissary/internal/datastore" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/api" @@ -10,11 +11,21 @@ import ( ) func (m *Mount) queryAgents(w http.ResponseWriter, r *http.Request) { - user, ok := assertRequestUser(w, r) + baseUser, ok := assertRequestUser(w, r) if !ok { return } + ctx := r.Context() + + user, ok := baseUser.(*userAuth.User) + if !ok { + logger.Error(ctx, "unexpected user type", logger.F("user", baseUser)) + api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil) + + return + } + limit, ok := getIntQueryParam(w, r, "limit", 10) if !ok { return @@ -28,7 +39,10 @@ func (m *Mount) queryAgents(w http.ResponseWriter, r *http.Request) { options := []datastore.AgentQueryOptionFunc{ datastore.WithAgentQueryLimit(int(limit)), datastore.WithAgentQueryOffset(int(offset)), - datastore.WithAgentQueryTenantID(user.Tenant()), + } + + if user.Role() != userAuth.RoleAdmin { + options = append(options, datastore.WithAgentQueryTenantID(user.Tenant())) } ids, ok := getIntSliceValues(w, r, "ids", nil) @@ -76,8 +90,6 @@ func (m *Mount) queryAgents(w http.ResponseWriter, r *http.Request) { options = append(options, datastore.WithAgentQueryStatus(agentStatuses...)) } - ctx := r.Context() - agents, total, err := m.agentRepo.Query( ctx, options..., diff --git a/internal/server/api/query_tenants.go b/internal/server/api/query_tenants.go new file mode 100644 index 0000000..576f01d --- /dev/null +++ b/internal/server/api/query_tenants.go @@ -0,0 +1,82 @@ +package api + +import ( + "net/http" + + userAuth "forge.cadoles.com/Cadoles/emissary/internal/auth/user" + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" + "gitlab.com/wpetit/goweb/logger" +) + +func (m *Mount) queryTenants(w http.ResponseWriter, r *http.Request) { + baseUser, ok := assertRequestUser(w, r) + if !ok { + return + } + + ctx := r.Context() + + user, ok := baseUser.(*userAuth.User) + if !ok { + logger.Error(ctx, "unexpected user type", logger.F("user", baseUser)) + api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil) + + return + } + + limit, ok := getIntQueryParam(w, r, "limit", 10) + if !ok { + return + } + + offset, ok := getIntQueryParam(w, r, "offset", 0) + if !ok { + return + } + + options := []datastore.TenantQueryOptionFunc{ + datastore.WithTenantQueryLimit(int(limit)), + datastore.WithTenantQueryOffset(int(offset)), + } + + ids, ok := getStringSliceValues(w, r, "ids", nil) + if !ok { + return + } + + tenantIDs := make([]datastore.TenantID, 0) + + if user.Role() != userAuth.RoleAdmin { + tenantIDs = append(tenantIDs, user.Tenant()) + } + + for _, id := range ids { + tenantIDs = append(tenantIDs, datastore.TenantID(id)) + } + + if len(tenantIDs) > 0 { + options = append(options, datastore.WithTenantQueryID(tenantIDs...)) + } + + tenants, total, err := m.tenantRepo.Query( + ctx, + options..., + ) + if err != nil { + err = errors.WithStack(err) + logger.Error(ctx, "could not list tenants", logger.CapturedE(err)) + api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil) + + return + } + + api.DataResponse(w, http.StatusOK, struct { + Tenants []*datastore.Tenant `json:"tenants"` + Total int `json:"total"` + }{ + Tenants: tenants, + Total: total, + }) +} diff --git a/pkg/client/query_tenants.go b/pkg/client/query_tenants.go new file mode 100644 index 0000000..5bf6394 --- /dev/null +++ b/pkg/client/query_tenants.go @@ -0,0 +1,77 @@ +package client + +import ( + "context" + "fmt" + "net/url" + + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "github.com/pkg/errors" +) + +type QueryTenantsOptionFunc func(*QueryTenantsOptions) + +type QueryTenantsOptions struct { + Options []OptionFunc + Limit *int + Offset *int + IDs []TenantID +} + +func WithQueryTenantsOptions(funcs ...OptionFunc) QueryTenantsOptionFunc { + return func(opts *QueryTenantsOptions) { + opts.Options = funcs + } +} + +func WithQueryTenantsLimit(limit int) QueryTenantsOptionFunc { + return func(opts *QueryTenantsOptions) { + opts.Limit = &limit + } +} + +func WithQueryTenantsOffset(offset int) QueryTenantsOptionFunc { + return func(opts *QueryTenantsOptions) { + opts.Offset = &offset + } +} + +func WithQueryTenantsID(ids ...datastore.TenantID) QueryTenantsOptionFunc { + return func(opts *QueryTenantsOptions) { + opts.IDs = ids + } +} + +func (c *Client) QueryTenants(ctx context.Context, funcs ...QueryTenantsOptionFunc) ([]*Tenant, int, error) { + options := &QueryTenantsOptions{} + for _, fn := range funcs { + fn(options) + } + + query := url.Values{} + + if options.IDs != nil && len(options.IDs) > 0 { + query.Set("ids", joinSlice(options.IDs)) + } + + path := fmt.Sprintf("/api/v1/tenants?%s", query.Encode()) + + response := withResponse[struct { + Tenants []*datastore.Tenant `json:"tenants"` + Total int `json:"total"` + }]() + + if options.Options == nil { + options.Options = make([]OptionFunc, 0) + } + + if err := c.apiGet(ctx, path, &response, options.Options...); err != nil { + return nil, 0, errors.WithStack(err) + } + + if response.Error != nil { + return nil, 0, errors.WithStack(response.Error) + } + + return response.Data.Tenants, response.Data.Total, nil +}