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
- Variables
- func BillingReport(ctx context.Context, cfg *config.Config, opts BillingReportOptions) (string, error)
- func CollectUsage(ctx context.Context, cfg *config.Config, opts CollectUsageOptions) error
- func Create(ctx context.Context, cfg *config.Config, opts CreateOptions) error
- func Destroy(ctx context.Context, cfg *config.Config, opts DestroyOptions) error
- func DisabledTableCount(report *LintRLSReport) int
- func GenerateRLSSQL(schema, table string) string
- func GenerateRemediationSQL(report *LintRLSReport) string
- func GenerateTenantColumnSQL(schema, table string) string
- func HasuraPermissionFilter() map[string]interface{}
- func HasuraRolePermissions() map[string][]string
- func IsValidPlan(p string) bool
- func JWTClaimsSchema() map[string]string
- func MigrationAuditLog() string
- func MigrationStripeOutbox() string
- func MigrationTenantsTable() string
- func MigrationUsageDaily() string
- func QueryUsage(ctx context.Context, cfg *config.Config, tenantID, month, format string) (string, error)
- func RetryStripeEvent(ctx context.Context, cfg *config.Config, eventID string) error
- func Suspend(ctx context.Context, cfg *config.Config, opts SuspendOptions) error
- func Upgrade(ctx context.Context, cfg *config.Config, opts UpgradeOptions) error
- type AllowlistEntry
- type AuditEntry
- type AuditOptions
- type BillingReportOptions
- type CollectUsageOptions
- type CoverageEntry
- type CreateOptions
- type DestroyOptions
- type LintRLSReport
- type LintResult
- type Plan
- type RLSPolicy
- type StripeOutboxEntry
- type SuspendOptions
- type Tenant
- type UpgradeOptions
- type UsageRecord
Constants ¶
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.
const ( RoleAnonymous = "anonymous" RoleUser = "user" RoleTenantMember = "tenant_member" RoleTenantAdmin = "tenant_admin" RoleAdmin = "admin" RoleService = "service" )
HasuraRole constants for multi-tenancy role model.
Variables ¶
var AllRoles = []string{ RoleAnonymous, RoleUser, RoleTenantMember, RoleTenantAdmin, RoleAdmin, RoleService, }
AllRoles lists all Hasura roles in permission inheritance order.
var ValidPlans = []Plan{ PlanBasic, PlanPro, PlanElite, PlanBusiness, PlanBusinessPlus, PlanEnterprise, }
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
HasuraRolePermissions returns the permission inheritance model for all 6 roles.
func IsValidPlan ¶
IsValidPlan reports whether p is a recognized plan name.
func JWTClaimsSchema ¶
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 ¶
RetryStripeEvent re-enqueues a failed Stripe outbox entry for retry. Uses a direct database/sql connection with $1 parameterized query (Path A).
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 ¶
CreateOptions holds flags for tenant creation.
type DestroyOptions ¶
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.
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 ¶
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 ¶
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 ¶
UpgradeOptions holds flags for tenant plan upgrade.