tenant

package
v1.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package tenant manages multi-tenant lifecycle operations for a self-hosted nSelf deployment.

SQL Injection Defense — SEC-SQL-02

SEC-SQL-02 has two execution paths:

Path A — direct database/sql connection (query functions):

QueryUsage, BillingReport, RetryStripeEvent, and Audit use openTenantDB to
obtain a *sql.DB connection to Postgres and execute queries with $1/$2…
positional-parameter binding via db.QueryContext/ExecContext. No value from
user input is ever interpolated into the SQL string on this path.

Path B — docker exec psql (write/collection functions):

CollectUsage and upsertUsage drive Postgres via
`docker exec <container> psql -c <sql>` because they perform multi-statement
INSERTs that reference CURRENT_DATE and Prometheus results unavailable over a
direct connection from the host. Values on this path are guarded by a two-layer
defence:
  1. Strict whitelist validation (validate* functions) — primary defence.
  2. sanitize() — belt-and-suspenders quote-doubling for the docker exec path
     only. It is intentionally kept for Path B; it is NOT used on Path A.

sanitize() is deprecated for SQL use. It exists only for the docker exec Path B functions that cannot migrate to direct connections. Do not add new callers.

Static analysis gate: CI runs the sec-lint.sh SQL-injection rule which fails if fmt.Sprintf with SQL keywords appears outside of caller-validated whitelist paths. See .github/workflows/sec-sqli-gate.yml.

Package tenant provides multi-tenancy management: RLS policy generation, tenant lifecycle (create/upgrade/suspend/destroy), usage metering, billing integration, and audit logging.

Index

Constants

View Source
const (
	MetricRowsStored     = "rows_stored"
	MetricStorageBytes   = "storage_bytes"
	MetricAITokensInput  = "ai_tokens_input"
	MetricAITokensOutput = "ai_tokens_output"
	MetricAICostUSD      = "ai_cost_usd"
	MetricBandwidthBytes = "bandwidth_bytes"
	MetricRequestsCount  = "requests_count"
	MetricActiveUsers    = "active_users"
)

Metric constants for usage_daily.

View Source
const (
	RoleAnonymous    = "anonymous"
	RoleUser         = "user"
	RoleTenantMember = "tenant_member"
	RoleTenantAdmin  = "tenant_admin"
	RoleAdmin        = "admin"
	RoleService      = "service"
)

HasuraRole constants for multi-tenancy role model.

Variables

AllRoles lists all Hasura roles in permission inheritance order.

ValidPlans lists all accepted plan identifiers.

Functions

func BillingReport

func BillingReport(ctx context.Context, cfg *config.Config, opts BillingReportOptions) (string, error)

BillingReport generates a usage and billing summary for a tenant or all tenants. Uses a direct database/sql connection with $N parameterized queries (Path A).

func CollectUsage

func CollectUsage(ctx context.Context, cfg *config.Config, opts CollectUsageOptions) error

CollectUsage runs the daily metering collection: pg catalog row counts, active users from auth sessions, MinIO bucket storage, and nginx bandwidth via Prometheus query API (when monitoring is enabled).

func Create

func Create(ctx context.Context, cfg *config.Config, opts CreateOptions) error

Create provisions a new tenant: inserts the tenants row and logs the creation in audit_log. Stripe Customer/Subscription creation is handled separately by the billing integration layer (requires STRIPE_SECRET_KEY).

func Destroy

func Destroy(ctx context.Context, cfg *config.Config, opts DestroyOptions) error

Destroy performs a hard delete of a tenant after backing up data. The --confirm-name flag must match the slug exactly.

func DisabledTableCount

func DisabledTableCount(report *LintRLSReport) int

DisabledTableCount returns the number of tables failing RLS lint. Useful for Prometheus metric emission.

func GenerateRLSSQL

func GenerateRLSSQL(schema, table string) string

GenerateRLSSQL returns the complete SQL string for enabling RLS on a table.

func GenerateRemediationSQL

func GenerateRemediationSQL(report *LintRLSReport) string

GenerateRemediationSQL produces migration SQL for all failing tables in a report.

func GenerateTenantColumnSQL

func GenerateTenantColumnSQL(schema, table string) string

GenerateTenantColumnSQL returns SQL to add tenant_id column, index, and FK to an existing table that does not yet have one. Identifiers are double-quoted via quoteIdent (SEC-17).

func HasuraPermissionFilter

func HasuraPermissionFilter() map[string]interface{}

HasuraPermissionFilter returns the Hasura permission filter JSON for tenant-scoped tables. This is applied to select/insert/update/delete permissions for tenant_member and tenant_admin roles.

func HasuraRolePermissions

func HasuraRolePermissions() map[string][]string

HasuraRolePermissions returns the permission inheritance model for all 6 roles.

func IsValidPlan

func IsValidPlan(p string) bool

IsValidPlan reports whether p is a recognized plan name.

func JWTClaimsSchema

func JWTClaimsSchema() map[string]string

JWTClaimsSchema returns the expected JWT claims structure for Hasura multi-tenancy. This is used to validate JWT configuration.

func MigrationAuditLog

func MigrationAuditLog() string

MigrationAuditLog returns the SQL to create nself_ops.audit_log. The table is INSERT-only (no UPDATE or DELETE permissions).

func MigrationStripeOutbox

func MigrationStripeOutbox() string

MigrationStripeOutbox returns the SQL to create nself_ops.stripe_outbox.

func MigrationTenantsTable

func MigrationTenantsTable() string

MigrationTenantsTable returns the SQL to create the core tenants table.

func MigrationUsageDaily

func MigrationUsageDaily() string

MigrationUsageDaily returns the SQL to create nself_ops.usage_daily.

func QueryUsage

func QueryUsage(ctx context.Context, cfg *config.Config, tenantID, month, format string) (string, error)

QueryUsage retrieves usage records for a tenant and optional month filter. Uses a direct database/sql connection with $N parameterized queries (Path A).

func RetryStripeEvent

func RetryStripeEvent(ctx context.Context, cfg *config.Config, eventID string) error

RetryStripeEvent re-enqueues a failed Stripe outbox entry for retry. Uses a direct database/sql connection with $1 parameterized query (Path A).

func Suspend

func Suspend(ctx context.Context, cfg *config.Config, opts SuspendOptions) error

Suspend marks a tenant as suspended with a reason. Stripe subscription pause is handled by the billing layer when STRIPE_SECRET_KEY is configured.

func Upgrade

func Upgrade(ctx context.Context, cfg *config.Config, opts UpgradeOptions) error

Upgrade changes a tenant's plan. Stripe Subscription update is handled by the billing layer when STRIPE_SECRET_KEY is configured.

Types

type AllowlistEntry

type AllowlistEntry struct {
	Schema string `json:"schema"`
	Table  string `json:"table"`
	Reason string `json:"reason"`
}

AllowlistEntry represents a table explicitly exempt from RLS requirements.

type AuditEntry

type AuditEntry struct {
	ID            string    `json:"id"`
	TenantID      string    `json:"tenant_id"`
	UserID        string    `json:"user_id"`
	Action        string    `json:"action"`
	ResourceType  string    `json:"resource_type"`
	ResourceID    string    `json:"resource_id"`
	IP            string    `json:"ip"`
	UserAgent     string    `json:"user_agent"`
	PayloadSHA256 string    `json:"payload_sha256"`
	Reason        string    `json:"reason,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
}

AuditEntry represents a row in nself_ops.audit_log.

func Audit

func Audit(ctx context.Context, cfg *config.Config, opts AuditOptions) ([]AuditEntry, error)

Audit queries nself_ops.audit_log for a specific tenant. Uses a direct database/sql connection with $N parameterized queries (Path A).

type AuditOptions

type AuditOptions struct {
	TenantID string
	Since    string // PostgreSQL interval, e.g. "7d" -> "7 days"
	Format   string // table or json
}

AuditOptions holds flags for querying the audit log.

type BillingReportOptions

type BillingReportOptions struct {
	TenantSlug string
	Month      string // YYYY-MM
	Format     string // table, json, csv
}

BillingReportOptions holds flags for billing report generation.

type CollectUsageOptions

type CollectUsageOptions struct {
	TenantID string // empty = all tenants
	Day      string // YYYY-MM-DD, empty = today
}

CollectUsageOptions holds flags for usage collection.

type CoverageEntry

type CoverageEntry struct {
	Schema string            `json:"schema"`
	Table  string            `json:"table"`
	Roles  map[string]string `json:"roles"` // role -> policy name or "none"
}

CoverageEntry maps a table to its per-role RLS policy coverage.

type CreateOptions

type CreateOptions struct {
	Slug string
	Plan Plan
}

CreateOptions holds flags for tenant creation.

type DestroyOptions

type DestroyOptions struct {
	Slug        string
	ConfirmName string // must match Slug for safety
}

DestroyOptions holds flags for tenant destruction.

type LintRLSReport

type LintRLSReport struct {
	Tables         []LintResult    `json:"tables"`
	TotalTables    int             `json:"total_tables"`
	RLSEnabled     int             `json:"rls_enabled"`
	RLSDisabled    int             `json:"rls_disabled"`
	Allowlisted    int             `json:"allowlisted"`
	Violations     int             `json:"violations"`
	CoverageMatrix []CoverageEntry `json:"coverage_matrix,omitempty"`
}

LintRLSReport is the top-level report returned by LintRLSFull.

func LintRLSFull

func LintRLSFull(ctx context.Context, cfg *config.Config, allowlist []AllowlistEntry) (*LintRLSReport, error)

LintRLSFull performs an exhaustive RLS audit of every np_* table (and any table with user_id or tenant_id columns). Tables in the allowlist are marked as passing with an annotation. Returns a full report with coverage matrix.

type LintResult

type LintResult struct {
	Schema      string   `json:"schema"`
	Table       string   `json:"table"`
	HasRLS      bool     `json:"rls_enabled"`
	HasPolicy   bool     `json:"has_policy"`
	PolicyCount int      `json:"policy_count"`
	Policies    []string `json:"policies,omitempty"`
	HasUserID   bool     `json:"has_user_id"`
	HasTenantID bool     `json:"has_tenant_id"`
	Allowlisted bool     `json:"allowlisted"`
	Reason      string   `json:"allowlist_reason,omitempty"`
	Pass        bool     `json:"pass"`
	Message     string   `json:"message"`
}

LintResult holds the outcome of an RLS lint check for a single table.

func LintRLS

func LintRLS(ctx context.Context, cfg *config.Config) ([]LintResult, error)

LintRLS scans pg_policies vs tables with a tenant_id column and reports any tables missing RLS policies. Returns non-nil error only on query failure; lint violations are reported in the results. This is the legacy function retained for backward compatibility.

type Plan

type Plan string

Plan represents a tenant billing plan tier.

const (
	PlanBasic        Plan = "basic"
	PlanPro          Plan = "pro"
	PlanElite        Plan = "elite"
	PlanBusiness     Plan = "business"
	PlanBusinessPlus Plan = "business-plus"
	PlanEnterprise   Plan = "enterprise"
)

type RLSPolicy

type RLSPolicy struct {
	TableName      string
	EnableSQL      string
	IsolationSQL   string
	AdminBypassSQL string
}

RLSPolicy holds the generated SQL for a single table's RLS policies.

func GenerateRLS

func GenerateRLS(schema, table string) *RLSPolicy

GenerateRLS produces the standard RLS policy SQL for a tenant-scoped table. The table must have a tenant_id UUID column. Two policies are created:

  • {table}_tenant_isolation: restricts rows to the JWT tenant_id claim
  • {table}_admin_bypass: allows nself_admin role full access

Identifiers are double-quoted via quoteIdent (SEC-17) so callers passing SQL meta-characters cannot break out of the identifier context.

type StripeOutboxEntry

type StripeOutboxEntry struct {
	ID          string     `json:"id"`
	TenantID    string     `json:"tenant_id"`
	Payload     string     `json:"payload"` // JSON
	CreatedAt   time.Time  `json:"created_at"`
	Attempts    int        `json:"attempts"`
	LastError   string     `json:"last_error,omitempty"`
	ProcessedAt *time.Time `json:"processed_at,omitempty"`
}

StripeOutboxEntry represents a row in nself_ops.stripe_outbox.

type SuspendOptions

type SuspendOptions struct {
	Slug   string
	Reason string
}

SuspendOptions holds flags for tenant suspension.

type Tenant

type Tenant struct {
	ID               string     `json:"id"`
	Slug             string     `json:"slug"`
	Plan             Plan       `json:"plan"`
	Status           string     `json:"status"` // active, suspended, destroyed
	StripeCustomerID string     `json:"stripe_customer_id,omitempty"`
	CreatedAt        time.Time  `json:"created_at"`
	SuspendedAt      *time.Time `json:"suspended_at,omitempty"`
	SuspendReason    string     `json:"suspend_reason,omitempty"`
	DestroyedAt      *time.Time `json:"destroyed_at,omitempty"`
}

Tenant represents a row in the tenants table.

type UpgradeOptions

type UpgradeOptions struct {
	Slug string
	Plan Plan
}

UpgradeOptions holds flags for tenant plan upgrade.

type UsageRecord

type UsageRecord struct {
	TenantID string  `json:"tenant_id"`
	Day      string  `json:"day"` // YYYY-MM-DD
	Metric   string  `json:"metric"`
	Value    float64 `json:"value"`
}

UsageRecord represents a row in nself_ops.usage_daily.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL