189 lines
3.9 KiB
Go
189 lines
3.9 KiB
Go
package serv
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/dosco/super-graph/psql"
|
|
"github.com/dosco/super-graph/qcode"
|
|
)
|
|
|
|
type stmt struct {
|
|
role *configRole
|
|
qc *qcode.QCode
|
|
skipped uint32
|
|
sql string
|
|
}
|
|
|
|
func buildStmt(qt qcode.QType, gql, vars []byte, role string) ([]stmt, error) {
|
|
switch qt {
|
|
case qcode.QTMutation:
|
|
return buildRoleStmt(gql, vars, role)
|
|
|
|
case qcode.QTQuery:
|
|
if role == "anon" {
|
|
return buildRoleStmt(gql, vars, "anon")
|
|
}
|
|
|
|
if conf.isABACEnabled() {
|
|
return buildMultiStmt(gql, vars)
|
|
}
|
|
|
|
return buildRoleStmt(gql, vars, "user")
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown query type '%d'", qt)
|
|
}
|
|
}
|
|
|
|
func buildRoleStmt(gql, vars []byte, role string) ([]stmt, error) {
|
|
ro, ok := conf.roles[role]
|
|
if !ok {
|
|
return nil, fmt.Errorf(`roles '%s' not defined in config`, role)
|
|
}
|
|
|
|
var vm map[string]json.RawMessage
|
|
var err error
|
|
|
|
if len(vars) != 0 {
|
|
if err := json.Unmarshal(vars, &vm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
qc, err := qcompile.Compile(gql, ro.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For the 'anon' role in production only compile
|
|
// queries for tables defined in the config file.
|
|
if conf.Production && ro.Name == "anon" && !hasTablesWithConfig(qc, ro) {
|
|
return nil, errors.New("query contains tables with no 'anon' role config")
|
|
}
|
|
|
|
stmts := []stmt{stmt{role: ro, qc: qc}}
|
|
w := &bytes.Buffer{}
|
|
|
|
skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stmts[0].skipped = skipped
|
|
stmts[0].sql = w.String()
|
|
|
|
return stmts, nil
|
|
}
|
|
|
|
func buildMultiStmt(gql, vars []byte) ([]stmt, error) {
|
|
var vm map[string]json.RawMessage
|
|
var err error
|
|
|
|
if len(vars) != 0 {
|
|
if err := json.Unmarshal(vars, &vm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(conf.RolesQuery) == 0 {
|
|
return nil, errors.New("roles_query not defined")
|
|
}
|
|
|
|
stmts := make([]stmt, 0, len(conf.Roles))
|
|
w := &bytes.Buffer{}
|
|
|
|
for i := 0; i < len(conf.Roles); i++ {
|
|
role := &conf.Roles[i]
|
|
|
|
// skip anon as it's not included in the combined multi-statement
|
|
if role.Name == "anon" {
|
|
continue
|
|
}
|
|
|
|
qc, err := qcompile.Compile(gql, role.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stmts = append(stmts, stmt{role: role, qc: qc})
|
|
|
|
skipped, err := pcompile.Compile(qc, w, psql.Variables(vm))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &stmts[len(stmts)-1]
|
|
s.skipped = skipped
|
|
s.sql = w.String()
|
|
w.Reset()
|
|
}
|
|
|
|
sql, err := renderUserQuery(stmts, vm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stmts[0].sql = sql
|
|
return stmts, nil
|
|
}
|
|
|
|
//nolint: errcheck
|
|
func renderUserQuery(
|
|
stmts []stmt, vars map[string]json.RawMessage) (string, error) {
|
|
w := &bytes.Buffer{}
|
|
|
|
io.WriteString(w, `SELECT "_sg_auth_info"."role", (CASE "_sg_auth_info"."role" `)
|
|
|
|
for _, s := range stmts {
|
|
if len(s.role.Match) == 0 &&
|
|
s.role.Name != "user" && s.role.Name != "anon" {
|
|
continue
|
|
}
|
|
io.WriteString(w, `WHEN '`)
|
|
io.WriteString(w, s.role.Name)
|
|
io.WriteString(w, `' THEN (`)
|
|
io.WriteString(w, s.sql)
|
|
io.WriteString(w, `) `)
|
|
}
|
|
|
|
io.WriteString(w, `END) FROM (SELECT (CASE WHEN EXISTS (`)
|
|
io.WriteString(w, conf.RolesQuery)
|
|
io.WriteString(w, `) THEN `)
|
|
|
|
io.WriteString(w, `(SELECT (CASE`)
|
|
for _, s := range stmts {
|
|
if len(s.role.Match) == 0 {
|
|
continue
|
|
}
|
|
io.WriteString(w, ` WHEN `)
|
|
io.WriteString(w, s.role.Match)
|
|
io.WriteString(w, ` THEN '`)
|
|
io.WriteString(w, s.role.Name)
|
|
io.WriteString(w, `'`)
|
|
}
|
|
|
|
io.WriteString(w, ` ELSE 'user' END) FROM (`)
|
|
io.WriteString(w, conf.RolesQuery)
|
|
io.WriteString(w, `) AS "_sg_auth_roles_query" LIMIT 1) `)
|
|
io.WriteString(w, `ELSE 'anon' END) FROM (VALUES (1)) AS "_sg_auth_filler") AS "_sg_auth_info"(role) LIMIT 1; `)
|
|
|
|
return w.String(), nil
|
|
}
|
|
|
|
func hasTablesWithConfig(qc *qcode.QCode, role *configRole) bool {
|
|
for _, id := range qc.Roots {
|
|
t, err := schema.GetTable(qc.Selects[id].Name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if _, ok := role.tablesMap[t.Name]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|