mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-18 04:27:23 +00:00
Re-add character-by-character alias validation that was present in the old tenantClauseNAlias but missing from the new WhereClauseAlias. Defense-in-depth against SQL injection via table alias parameter.
85 lines
2.6 KiB
Go
85 lines
2.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// QueryScope represents the multi-level isolation scope for database queries.
|
|
// Currently supports tenant-level scoping; designed to be extended with
|
|
// project-level scoping when the Project feature is implemented.
|
|
type QueryScope struct {
|
|
TenantID uuid.UUID
|
|
ProjectID *uuid.UUID // nil = no project filter (tenant-only)
|
|
}
|
|
|
|
// ScopeFromContext extracts the query scope from context.
|
|
// Returns error if tenant ID is missing (fail-closed).
|
|
func ScopeFromContext(ctx context.Context) (QueryScope, error) {
|
|
tid := TenantIDFromContext(ctx)
|
|
if tid == uuid.Nil {
|
|
return QueryScope{}, fmt.Errorf("tenant_id required")
|
|
}
|
|
return QueryScope{TenantID: tid}, nil
|
|
}
|
|
|
|
// WhereClause generates SQL WHERE conditions for the scope.
|
|
// Returns the clause string (e.g. " AND tenant_id = $3"), args, and the next
|
|
// available parameter index. Callers chain: startParam → nextParam.
|
|
func (s QueryScope) WhereClause(startParam int) (clause string, args []any, nextParam int) {
|
|
clause = fmt.Sprintf(" AND tenant_id = $%d", startParam)
|
|
args = []any{s.TenantID}
|
|
nextParam = startParam + 1
|
|
|
|
if s.ProjectID != nil {
|
|
clause += fmt.Sprintf(" AND project_id = $%d", nextParam)
|
|
args = append(args, *s.ProjectID)
|
|
nextParam++
|
|
}
|
|
|
|
return clause, args, nextParam
|
|
}
|
|
|
|
// WhereClauseAlias generates SQL WHERE conditions qualified with a table alias.
|
|
// Used in JOIN queries to avoid column ambiguity.
|
|
// SECURITY: alias is interpolated into SQL — callers MUST pass hardcoded string literals only.
|
|
func (s QueryScope) WhereClauseAlias(startParam int, alias string) (clause string, args []any, nextParam int) {
|
|
// Defense-in-depth: only allow simple alphanumeric aliases to prevent SQL injection.
|
|
for _, c := range alias {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
|
|
return "", nil, startParam
|
|
}
|
|
}
|
|
clause = fmt.Sprintf(" AND %s.tenant_id = $%d", alias, startParam)
|
|
args = []any{s.TenantID}
|
|
nextParam = startParam + 1
|
|
|
|
if s.ProjectID != nil {
|
|
clause += fmt.Sprintf(" AND %s.project_id = $%d", alias, nextParam)
|
|
args = append(args, *s.ProjectID)
|
|
nextParam++
|
|
}
|
|
|
|
return clause, args, nextParam
|
|
}
|
|
|
|
// InsertValues returns column names and values for INSERT operations.
|
|
// Falls back to MasterTenantID when TenantID is nil.
|
|
func (s QueryScope) InsertValues() (columns []string, values []any) {
|
|
tid := s.TenantID
|
|
if tid == uuid.Nil {
|
|
tid = MasterTenantID
|
|
}
|
|
columns = []string{"tenant_id"}
|
|
values = []any{tid}
|
|
|
|
if s.ProjectID != nil {
|
|
columns = append(columns, "project_id")
|
|
values = append(values, *s.ProjectID)
|
|
}
|
|
|
|
return columns, values
|
|
}
|