Resources segregation by tenant #20

Merged
wpetit merged 7 commits from tenant into master 2024-02-29 15:33:30 +01:00
8 changed files with 350 additions and 4 deletions
Showing only changes of commit 954597d241 - Show all commits

View File

@ -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
},
}
}

View File

@ -13,6 +13,7 @@ func Root() *cli.Command {
GetCommand(),
UpdateCommand(),
DeleteCommand(),
QueryCommand(),
},
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)

View File

@ -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...,

View File

@ -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,
})
}

View File

@ -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
}