Add named auth and the all new action endpoints
This commit is contained in:
parent
1a3d74e1ce
commit
62fd1eac55
|
@ -167,10 +167,13 @@ roles:
|
|||
block: false
|
||||
|
||||
- name: deals
|
||||
|
||||
query:
|
||||
limit: 3
|
||||
columns: ["name", "description" ]
|
||||
aggregation: false
|
||||
|
||||
- name: purchases
|
||||
query:
|
||||
limit: 3
|
||||
aggregation: false
|
||||
|
||||
- name: user
|
||||
|
@ -183,12 +186,10 @@ roles:
|
|||
query:
|
||||
limit: 50
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description", "search_rank", "search_headline_description" ]
|
||||
disable_functions: false
|
||||
|
||||
insert:
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description" ]
|
||||
presets:
|
||||
- user_id: "$user_id"
|
||||
- created_at: "now"
|
||||
|
|
|
@ -1319,7 +1319,7 @@ auth:
|
|||
max_active: 12000
|
||||
```
|
||||
|
||||
### JWT Token Auth
|
||||
### JWT Tokens
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
|
@ -1339,6 +1339,51 @@ We can get the JWT token either from the `authorization` header where we expect
|
|||
|
||||
For validation a `secret` or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
|
||||
|
||||
### HTTP Headers
|
||||
|
||||
```yaml
|
||||
header:
|
||||
name: X-AppEngine-QueueName
|
||||
exists: true
|
||||
#value: default
|
||||
```
|
||||
|
||||
Header auth is usually the best option to authenticate requests to the action endpoints. For example you
|
||||
might want to use an action to refresh a materalized view every hour and only want a cron service like the Google AppEngine Cron service to make that request in this case a config similar to the one above will do.
|
||||
|
||||
The `exists: true` parameter ensures that only the existance of the header is checked not its value. The `value` parameter lets you confirm that the value matches the one assgined to the parameter. This helps in the case you are using a shared secret to protect the endpoint.
|
||||
|
||||
### Named Auth
|
||||
|
||||
```yaml
|
||||
# You can add additional named auths to use with actions
|
||||
# In this example actions using this auth can only be
|
||||
# called from the Google Appengine Cron service that
|
||||
# sets a special header to all it's requests
|
||||
auths:
|
||||
- name: from_taskqueue
|
||||
type: header
|
||||
header:
|
||||
name: X-Appengine-Cron
|
||||
exists: true
|
||||
```
|
||||
|
||||
In addition to the default auth configuration you can create additional named auth configurations to be used
|
||||
with features like `actions`. For example while your main GraphQL endpoint uses JWT for authentication you may want to use a header value to ensure your actions can only be called by clients having access to a shared secret
|
||||
or security header.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions is a very useful feature that is currently work in progress. For now the best use case for actions is to
|
||||
refresh database tables like materialized views or call a database procedure to refresh a cache table, etc. An action creates an http endpoint that anyone can call to have the SQL query executed. The below example will create an endpoint `/api/v1/actions/refresh_leaderboard_users` any request send to that endpoint will cause the sql query to be executed. the `auth_name` points to a named auth that should be used to secure this endpoint. In future we have big plans to allow your own custom code to run using actions.
|
||||
|
||||
```yaml
|
||||
actions:
|
||||
- name: refresh_leaderboard_users
|
||||
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||
auth_name: from_taskqueue
|
||||
```
|
||||
|
||||
#### Using CURL to test a query
|
||||
|
||||
```bash
|
||||
|
@ -1593,6 +1638,22 @@ auth:
|
|||
# public_key_file: /secrets/public_key.pem
|
||||
# public_key_type: ecdsa #rsa
|
||||
|
||||
# header:
|
||||
# name: dnt
|
||||
# exists: true
|
||||
# value: localhost:8080
|
||||
|
||||
# You can add additional named auths to use with actions
|
||||
# In this example actions using this auth can only be
|
||||
# called from the Google Appengine Cron service that
|
||||
# sets a special header to all it's requests
|
||||
auths:
|
||||
- name: from_taskqueue
|
||||
type: header
|
||||
header:
|
||||
name: X-Appengine-Cron
|
||||
exists: true
|
||||
|
||||
database:
|
||||
type: postgres
|
||||
host: db
|
||||
|
@ -1623,6 +1684,17 @@ database:
|
|||
- encrypted
|
||||
- token
|
||||
|
||||
# Create custom actions with their own api endpoints
|
||||
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
||||
# A request to this url will execute the configured SQL query
|
||||
# which in this case refreshes a materialized view in the database.
|
||||
# The auth_name is from one of the configured auths
|
||||
actions:
|
||||
- name: refresh_leaderboard_users
|
||||
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||
auth_name: from_taskqueue
|
||||
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
remotes:
|
||||
|
|
|
@ -151,6 +151,7 @@ SELECT
|
|||
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
|
||||
CASE
|
||||
WHEN f.attndims != 0 THEN true
|
||||
WHEN right(pg_catalog.format_type(f.atttypid,f.atttypmod), 2) = '[]' THEN true
|
||||
ELSE false
|
||||
END AS array,
|
||||
CASE
|
||||
|
@ -175,7 +176,7 @@ FROM pg_attribute f
|
|||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
|
||||
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
|
||||
WHERE c.relkind = ('r'::char)
|
||||
WHERE c.relkind IN ('r', 'v', 'm', 'f')
|
||||
AND n.nspname = $1 -- Replace with Schema name
|
||||
AND c.relname = $2 -- Replace with table name
|
||||
AND f.attnum > 0
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package serv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type actionFn func(w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
func newAction(a configAction) (http.Handler, error) {
|
||||
var fn actionFn
|
||||
var err error
|
||||
|
||||
if len(a.SQL) != 0 {
|
||||
fn, err = newSQLAction(a)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid config for action '%s'", a.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpFn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := fn(w, r); err != nil {
|
||||
errlog.Error().Err(err).Send()
|
||||
errorResp(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
return http.HandlerFunc(httpFn), nil
|
||||
}
|
||||
|
||||
func newSQLAction(a configAction) (actionFn, error) {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) error {
|
||||
_, err := db.Exec(r.Context(), a.SQL)
|
||||
return err
|
||||
}
|
||||
|
||||
return fn, nil
|
||||
}
|
60
serv/auth.go
60
serv/auth.go
|
@ -3,7 +3,6 @@ package serv
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ctxkey int
|
||||
|
@ -14,7 +13,7 @@ const (
|
|||
userRoleKey
|
||||
)
|
||||
|
||||
func headerAuth(next http.Handler) http.HandlerFunc {
|
||||
func headerAuth(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
|
@ -37,28 +36,53 @@ func headerAuth(next http.Handler) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func withAuth(next http.Handler) http.Handler {
|
||||
at := conf.Auth.Type
|
||||
ru := conf.Auth.Rails.URL
|
||||
func headerHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
hdr := authc.Header
|
||||
|
||||
if conf.Auth.CredsInHeader {
|
||||
next = headerAuth(next)
|
||||
if len(hdr.Name) == 0 {
|
||||
errlog.Fatal().Str("auth", authc.Name).Msg("no header.name defined")
|
||||
}
|
||||
|
||||
switch at {
|
||||
if !hdr.Exists && len(hdr.Value) == 0 {
|
||||
errlog.Fatal().Str("auth", authc.Name).Msg("no header.value defined")
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var fo1 bool
|
||||
value := r.Header.Get(hdr.Name)
|
||||
|
||||
switch {
|
||||
case hdr.Exists:
|
||||
fo1 = (len(value) == 0)
|
||||
|
||||
default:
|
||||
fo1 = (value != hdr.Value)
|
||||
}
|
||||
|
||||
if fo1 {
|
||||
http.Error(w, "401 unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func withAuth(next http.Handler, authc configAuth) http.Handler {
|
||||
if authc.CredsInHeader {
|
||||
next = headerAuth(authc, next)
|
||||
}
|
||||
|
||||
switch authc.Type {
|
||||
case "rails":
|
||||
if strings.HasPrefix(ru, "memcache:") {
|
||||
return railsMemcacheHandler(next)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ru, "redis:") {
|
||||
return railsRedisHandler(next)
|
||||
}
|
||||
|
||||
return railsCookieHandler(next)
|
||||
return railsHandler(authc, next)
|
||||
|
||||
case "jwt":
|
||||
return jwtHandler(next)
|
||||
return jwtHandler(authc, next)
|
||||
|
||||
case "header":
|
||||
return headerHandler(authc, next)
|
||||
|
||||
}
|
||||
|
||||
return next
|
||||
|
|
|
@ -14,18 +14,18 @@ const (
|
|||
jwtAuth0 int = iota + 1
|
||||
)
|
||||
|
||||
func jwtHandler(next http.Handler) http.HandlerFunc {
|
||||
func jwtHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
var key interface{}
|
||||
var jwtProvider int
|
||||
|
||||
cookie := conf.Auth.Cookie
|
||||
cookie := authc.Cookie
|
||||
|
||||
if conf.Auth.JWT.Provider == "auth0" {
|
||||
if authc.JWT.Provider == "auth0" {
|
||||
jwtProvider = jwtAuth0
|
||||
}
|
||||
|
||||
secret := conf.Auth.JWT.Secret
|
||||
publicKeyFile := conf.Auth.JWT.PubKeyFile
|
||||
secret := authc.JWT.Secret
|
||||
publicKeyFile := authc.JWT.PubKeyFile
|
||||
|
||||
switch {
|
||||
case len(secret) != 0:
|
||||
|
@ -37,7 +37,7 @@ func jwtHandler(next http.Handler) http.HandlerFunc {
|
|||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
switch conf.Auth.JWT.PubKeyType {
|
||||
switch authc.JWT.PubKeyType {
|
||||
case "ecdsa":
|
||||
key, err = jwt.ParseECPublicKeyFromPEM(kd)
|
||||
|
||||
|
|
|
@ -6,32 +6,47 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/dosco/super-graph/rails"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
)
|
||||
|
||||
func railsRedisHandler(next http.Handler) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
func railsHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
ru := authc.Rails.URL
|
||||
|
||||
if strings.HasPrefix(ru, "memcache:") {
|
||||
return railsMemcacheHandler(authc, next)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ru, "redis:") {
|
||||
return railsRedisHandler(authc, next)
|
||||
}
|
||||
|
||||
return railsCookieHandler(authc, next)
|
||||
}
|
||||
|
||||
func railsRedisHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
cookie := authc.Cookie
|
||||
if len(cookie) == 0 {
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.URL) == 0 {
|
||||
if len(authc.Rails.URL) == 0 {
|
||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||
}
|
||||
|
||||
rp := &redis.Pool{
|
||||
MaxIdle: conf.Auth.Rails.MaxIdle,
|
||||
MaxActive: conf.Auth.Rails.MaxActive,
|
||||
MaxIdle: authc.Rails.MaxIdle,
|
||||
MaxActive: authc.Rails.MaxActive,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
c, err := redis.DialURL(conf.Auth.Rails.URL)
|
||||
c, err := redis.DialURL(authc.Rails.URL)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
pwd := conf.Auth.Rails.Password
|
||||
pwd := authc.Rails.Password
|
||||
if len(pwd) != 0 {
|
||||
if _, err := c.Do("AUTH", pwd); err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
|
@ -66,17 +81,17 @@ func railsRedisHandler(next http.Handler) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
func railsMemcacheHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
cookie := authc.Cookie
|
||||
if len(cookie) == 0 {
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.URL) == 0 {
|
||||
if len(authc.Rails.URL) == 0 {
|
||||
errlog.Fatal().Msg("no auth.rails.url defined")
|
||||
}
|
||||
|
||||
rURL, err := url.Parse(conf.Auth.Rails.URL)
|
||||
rURL, err := url.Parse(authc.Rails.URL)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
@ -108,13 +123,13 @@ func railsMemcacheHandler(next http.Handler) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func railsCookieHandler(next http.Handler) http.HandlerFunc {
|
||||
cookie := conf.Auth.Cookie
|
||||
func railsCookieHandler(authc configAuth, next http.Handler) http.HandlerFunc {
|
||||
cookie := authc.Cookie
|
||||
if len(cookie) == 0 {
|
||||
errlog.Fatal().Msg("no auth.cookie defined")
|
||||
}
|
||||
|
||||
ra, err := railsAuth(conf)
|
||||
ra, err := railsAuth(authc)
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
@ -139,13 +154,13 @@ func railsCookieHandler(next http.Handler) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func railsAuth(c *config) (*rails.Auth, error) {
|
||||
secret := c.Auth.Rails.SecretKeyBase
|
||||
func railsAuth(authc configAuth) (*rails.Auth, error) {
|
||||
secret := authc.Rails.SecretKeyBase
|
||||
if len(secret) == 0 {
|
||||
return nil, errors.New("no auth.rails.secret_key_base defined")
|
||||
}
|
||||
|
||||
version := c.Auth.Rails.Version
|
||||
version := authc.Rails.Version
|
||||
if len(version) == 0 {
|
||||
return nil, errors.New("no auth.rails.version defined")
|
||||
}
|
||||
|
@ -155,16 +170,16 @@ func railsAuth(c *config) (*rails.Auth, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Auth.Rails.Salt) != 0 {
|
||||
ra.Salt = c.Auth.Rails.Salt
|
||||
if len(authc.Rails.Salt) != 0 {
|
||||
ra.Salt = authc.Rails.Salt
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.SignSalt) != 0 {
|
||||
ra.SignSalt = c.Auth.Rails.SignSalt
|
||||
if len(authc.Rails.SignSalt) != 0 {
|
||||
ra.SignSalt = authc.Rails.SignSalt
|
||||
}
|
||||
|
||||
if len(conf.Auth.Rails.AuthSalt) != 0 {
|
||||
ra.AuthSalt = c.Auth.Rails.AuthSalt
|
||||
if len(authc.Rails.AuthSalt) != 0 {
|
||||
ra.AuthSalt = authc.Rails.AuthSalt
|
||||
}
|
||||
|
||||
return ra, nil
|
||||
|
|
|
@ -311,3 +311,13 @@ func getMigrationVars() map[string]interface{} {
|
|||
"env": strings.ToLower(os.Getenv("GO_ENV")),
|
||||
}
|
||||
}
|
||||
|
||||
func initConfOnce() {
|
||||
var err error
|
||||
|
||||
if conf == nil {
|
||||
if conf, err = initConf(); err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,23 +7,22 @@ import (
|
|||
func cmdServ(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
initWatcher(confPath)
|
||||
|
||||
if conf, err = initConf(); err != nil {
|
||||
fatalInProd(err, "failed to read config")
|
||||
}
|
||||
|
||||
if conf != nil {
|
||||
db, err = initDBPool(conf)
|
||||
db, err = initDBPool(conf)
|
||||
|
||||
if err == nil {
|
||||
initCompiler()
|
||||
initAllowList(confPath)
|
||||
initPreparedList(confPath)
|
||||
} else {
|
||||
fatalInProd(err, "failed to connect to database")
|
||||
}
|
||||
if err != nil {
|
||||
fatalInProd(err, "failed to connect to database")
|
||||
}
|
||||
|
||||
initWatcher(confPath)
|
||||
initCompiler()
|
||||
initResolvers()
|
||||
initAllowList(confPath)
|
||||
initPreparedList(confPath)
|
||||
|
||||
startHTTP()
|
||||
}
|
||||
|
|
128
serv/config.go
128
serv/config.go
|
@ -33,30 +33,8 @@ type config struct {
|
|||
|
||||
Inflections map[string]string
|
||||
|
||||
Auth struct {
|
||||
Type string
|
||||
Cookie string
|
||||
CredsInHeader bool `mapstructure:"creds_in_header"`
|
||||
|
||||
Rails struct {
|
||||
Version string
|
||||
SecretKeyBase string `mapstructure:"secret_key_base"`
|
||||
URL string
|
||||
Password string
|
||||
MaxIdle int `mapstructure:"max_idle"`
|
||||
MaxActive int `mapstructure:"max_active"`
|
||||
Salt string
|
||||
SignSalt string `mapstructure:"sign_salt"`
|
||||
AuthSalt string `mapstructure:"auth_salt"`
|
||||
}
|
||||
|
||||
JWT struct {
|
||||
Provider string
|
||||
Secret string
|
||||
PubKeyFile string `mapstructure:"public_key_file"`
|
||||
PubKeyType string `mapstructure:"public_key_type"`
|
||||
}
|
||||
}
|
||||
Auth configAuth
|
||||
Auths []configAuth
|
||||
|
||||
DB struct {
|
||||
Type string
|
||||
|
@ -77,6 +55,8 @@ type config struct {
|
|||
Tables []configTable
|
||||
} `mapstructure:"database"`
|
||||
|
||||
Actions []configAction
|
||||
|
||||
Tables []configTable
|
||||
|
||||
RolesQuery string `mapstructure:"roles_query"`
|
||||
|
@ -85,6 +65,38 @@ type config struct {
|
|||
abacEnabled bool
|
||||
}
|
||||
|
||||
type configAuth struct {
|
||||
Name string
|
||||
Type string
|
||||
Cookie string
|
||||
CredsInHeader bool `mapstructure:"creds_in_header"`
|
||||
|
||||
Rails struct {
|
||||
Version string
|
||||
SecretKeyBase string `mapstructure:"secret_key_base"`
|
||||
URL string
|
||||
Password string
|
||||
MaxIdle int `mapstructure:"max_idle"`
|
||||
MaxActive int `mapstructure:"max_active"`
|
||||
Salt string
|
||||
SignSalt string `mapstructure:"sign_salt"`
|
||||
AuthSalt string `mapstructure:"auth_salt"`
|
||||
}
|
||||
|
||||
JWT struct {
|
||||
Provider string
|
||||
Secret string
|
||||
PubKeyFile string `mapstructure:"public_key_file"`
|
||||
PubKeyType string `mapstructure:"public_key_type"`
|
||||
}
|
||||
|
||||
Header struct {
|
||||
Name string
|
||||
Value string
|
||||
Exists bool
|
||||
}
|
||||
}
|
||||
|
||||
type configColumn struct {
|
||||
Name string
|
||||
Type string
|
||||
|
@ -156,6 +168,12 @@ type configRole struct {
|
|||
tablesMap map[string]*configRoleTable
|
||||
}
|
||||
|
||||
type configAction struct {
|
||||
Name string
|
||||
SQL string
|
||||
AuthName string `mapstructure:"auth_name"`
|
||||
}
|
||||
|
||||
func newConfig(name string) *viper.Viper {
|
||||
vi := viper.New()
|
||||
|
||||
|
@ -283,26 +301,48 @@ func (c *config) Init(vi *viper.Viper) error {
|
|||
func (c *config) validate() {
|
||||
rm := make(map[string]struct{})
|
||||
|
||||
for i := range c.Roles {
|
||||
name := c.Roles[i].Name
|
||||
for _, v := range c.Roles {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if _, ok := rm[name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate config for role '%s'", c.Roles[i].Name)
|
||||
errlog.Fatal().Msgf("duplicate config for role '%s'", v.Name)
|
||||
}
|
||||
rm[name] = struct{}{}
|
||||
}
|
||||
|
||||
tm := make(map[string]struct{})
|
||||
|
||||
for i := range c.Tables {
|
||||
name := c.Tables[i].Name
|
||||
for _, v := range c.Tables {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if _, ok := tm[name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate config for table '%s'", c.Tables[i].Name)
|
||||
errlog.Fatal().Msgf("duplicate config for table '%s'", v.Name)
|
||||
}
|
||||
tm[name] = struct{}{}
|
||||
}
|
||||
|
||||
am := make(map[string]struct{})
|
||||
|
||||
for _, v := range c.Auths {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if _, ok := am[name]; ok {
|
||||
errlog.Fatal().Msgf("duplicate config for auth '%s'", v.Name)
|
||||
}
|
||||
am[name] = struct{}{}
|
||||
}
|
||||
|
||||
for _, v := range c.Actions {
|
||||
if len(v.AuthName) == 0 {
|
||||
continue
|
||||
}
|
||||
authName := strings.ToLower(v.AuthName)
|
||||
|
||||
if _, ok := am[authName]; !ok {
|
||||
errlog.Fatal().Msgf("invalid auth_name for action '%s'", v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.RolesQuery) == 0 {
|
||||
logger.Warn().Msgf("no 'roles_query' defined.")
|
||||
}
|
||||
|
@ -349,3 +389,31 @@ func sanitize(s string) string {
|
|||
return strings.ToLower(m)
|
||||
})
|
||||
}
|
||||
|
||||
func getConfigName() string {
|
||||
if len(os.Getenv("GO_ENV")) == 0 {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
ge := strings.ToLower(os.Getenv("GO_ENV"))
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ge, "pro"):
|
||||
return "prod"
|
||||
|
||||
case strings.HasPrefix(ge, "sta"):
|
||||
return "stage"
|
||||
|
||||
case strings.HasPrefix(ge, "tes"):
|
||||
return "test"
|
||||
|
||||
case strings.HasPrefix(ge, "dev"):
|
||||
return "dev"
|
||||
}
|
||||
|
||||
return ge
|
||||
}
|
||||
|
||||
func isDev() bool {
|
||||
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
|
||||
}
|
||||
|
|
14
serv/init.go
14
serv/init.go
|
@ -148,20 +148,6 @@ func initCompiler() {
|
|||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize compilers")
|
||||
}
|
||||
|
||||
if err := initResolvers(); err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialized resolvers")
|
||||
}
|
||||
}
|
||||
|
||||
func initConfOnce() {
|
||||
var err error
|
||||
|
||||
if conf == nil {
|
||||
if conf, err = initConf(); err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to read config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initAllowList(cpath string) {
|
||||
|
|
|
@ -22,16 +22,20 @@ type resolvFn struct {
|
|||
Fn func(h http.Header, id []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
func initResolvers() error {
|
||||
func initResolvers() {
|
||||
var err error
|
||||
rmap = make(map[uint64]*resolvFn)
|
||||
|
||||
for _, t := range conf.Tables {
|
||||
err := initRemotes(t)
|
||||
err = initRemotes(t)
|
||||
if err != nil {
|
||||
return err
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Msg("failed to initialize resolvers")
|
||||
}
|
||||
}
|
||||
|
||||
func initRemotes(t configTable) error {
|
98
serv/serv.go
98
serv/serv.go
|
@ -101,9 +101,14 @@ func startHTTP() {
|
|||
hostPort = defaultHP
|
||||
}
|
||||
|
||||
routes, err := routeHandler()
|
||||
if err != nil {
|
||||
errlog.Fatal().Err(err).Send()
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: hostPort,
|
||||
Handler: routeHandler(),
|
||||
Handler: routes,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
|
@ -140,59 +145,74 @@ func startHTTP() {
|
|||
<-idleConnsClosed
|
||||
}
|
||||
|
||||
func routeHandler() http.Handler {
|
||||
var apiH http.Handler
|
||||
|
||||
if conf != nil && conf.HTTPGZip {
|
||||
gzipH := gziphandler.MustNewGzipLevelHandler(6)
|
||||
apiH = gzipH(http.HandlerFunc(apiV1))
|
||||
} else {
|
||||
apiH = http.HandlerFunc(apiV1)
|
||||
}
|
||||
|
||||
func routeHandler() (http.Handler, error) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
if conf != nil {
|
||||
mux.HandleFunc("/health", health)
|
||||
mux.Handle("/api/v1/graphql", withAuth(apiH))
|
||||
if conf == nil {
|
||||
return mux, nil
|
||||
}
|
||||
|
||||
if conf.WebUI {
|
||||
mux.Handle("/", http.FileServer(rice.MustFindBox("../web/build").HTTPBox()))
|
||||
routes := map[string]http.Handler{
|
||||
"/health": http.HandlerFunc(health),
|
||||
"/api/v1/graphql": withAuth(http.HandlerFunc(apiV1), conf.Auth),
|
||||
}
|
||||
|
||||
if err := setActionRoutes(routes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.WebUI {
|
||||
routes["/"] = http.FileServer(rice.MustFindBox("../web/build").HTTPBox())
|
||||
}
|
||||
|
||||
if conf.HTTPGZip {
|
||||
gz := gziphandler.MustNewGzipLevelHandler(6)
|
||||
for k, v := range routes {
|
||||
routes[k] = gz(v)
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range routes {
|
||||
mux.Handle(k, v)
|
||||
}
|
||||
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Server", serverName)
|
||||
mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
return http.HandlerFunc(fn), nil
|
||||
}
|
||||
|
||||
func getConfigName() string {
|
||||
if len(os.Getenv("GO_ENV")) == 0 {
|
||||
return "dev"
|
||||
func setActionRoutes(routes map[string]http.Handler) error {
|
||||
var err error
|
||||
|
||||
for _, a := range conf.Actions {
|
||||
var fn http.Handler
|
||||
|
||||
fn, err = newAction(a)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
p := fmt.Sprintf("/api/v1/actions/%s", strings.ToLower(a.Name))
|
||||
|
||||
if authc, ok := findAuth(a.AuthName); ok {
|
||||
routes[p] = withAuth(fn, authc)
|
||||
} else {
|
||||
routes[p] = fn
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ge := strings.ToLower(os.Getenv("GO_ENV"))
|
||||
func findAuth(name string) (configAuth, bool) {
|
||||
var authc configAuth
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(ge, "pro"):
|
||||
return "prod"
|
||||
|
||||
case strings.HasPrefix(ge, "sta"):
|
||||
return "stage"
|
||||
|
||||
case strings.HasPrefix(ge, "tes"):
|
||||
return "test"
|
||||
|
||||
case strings.HasPrefix(ge, "dev"):
|
||||
return "dev"
|
||||
for _, a := range conf.Auths {
|
||||
if strings.EqualFold(a.Name, name) {
|
||||
return a, true
|
||||
}
|
||||
}
|
||||
|
||||
return ge
|
||||
}
|
||||
|
||||
func isDev() bool {
|
||||
return strings.HasPrefix(os.Getenv("GO_ENV"), "dev")
|
||||
return authc, false
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/dosco/super-graph/jsn"
|
||||
|
@ -127,9 +128,14 @@ func findStmt(role string, stmts []stmt) *stmt {
|
|||
}
|
||||
|
||||
func fatalInProd(err error, msg string) {
|
||||
if isDev() {
|
||||
errlog.Error().Err(err).Msg(msg)
|
||||
} else {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if !isDev() {
|
||||
errlog.Fatal().Err(err).Msg(msg)
|
||||
}
|
||||
|
||||
errlog.Error().Err(err).Msg(msg)
|
||||
|
||||
wg.Add(1)
|
||||
wg.Wait()
|
||||
}
|
||||
|
|
35
tmpl/dev.yml
35
tmpl/dev.yml
|
@ -49,7 +49,7 @@ migrations_path: ./config/migrations
|
|||
# sheep: sheep
|
||||
|
||||
auth:
|
||||
# Can be 'rails' or 'jwt'
|
||||
# Can be 'rails', 'jwt' or 'header'
|
||||
type: rails
|
||||
cookie: _{% app_name_slug %}_session
|
||||
|
||||
|
@ -83,6 +83,22 @@ auth:
|
|||
# public_key_file: /secrets/public_key.pem
|
||||
# public_key_type: ecdsa #rsa
|
||||
|
||||
# header:
|
||||
# name: dnt
|
||||
# exists: true
|
||||
# value: localhost:8080
|
||||
|
||||
# You can add additional named auths to use with actions
|
||||
# In this example actions using this auth can only be
|
||||
# called from the Google Appengine Cron service that
|
||||
# sets a special header to all it's requests
|
||||
auths:
|
||||
- name: from_taskqueue
|
||||
type: header
|
||||
header:
|
||||
name: X-Appengine-Cron
|
||||
exists: true
|
||||
|
||||
database:
|
||||
type: postgres
|
||||
host: db
|
||||
|
@ -116,6 +132,16 @@ database:
|
|||
- encrypted
|
||||
- token
|
||||
|
||||
# Create custom actions with their own api endpoints
|
||||
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
||||
# A request to this url will execute the configured SQL query
|
||||
# which in this case refreshes a materialized view in the database.
|
||||
# The auth_name is from one of the configured auths
|
||||
actions:
|
||||
- name: refresh_leaderboard_users
|
||||
sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
||||
auth_name: from_taskqueue
|
||||
|
||||
tables:
|
||||
- name: customers
|
||||
remotes:
|
||||
|
@ -137,6 +163,7 @@ tables:
|
|||
name: me
|
||||
table: users
|
||||
|
||||
|
||||
roles_query: "SELECT * FROM users WHERE id = $user_id"
|
||||
|
||||
roles:
|
||||
|
@ -168,20 +195,16 @@ roles:
|
|||
query:
|
||||
limit: 50
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description" ]
|
||||
disable_functions: false
|
||||
|
||||
insert:
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns: ["id", "name", "description" ]
|
||||
presets:
|
||||
- user_id: "$user_id"
|
||||
- created_at: "now"
|
||||
|
||||
update:
|
||||
filters: ["{ user_id: { eq: $user_id } }"]
|
||||
columns:
|
||||
- id
|
||||
- name
|
||||
presets:
|
||||
- updated_at: "now"
|
||||
|
||||
|
|
Loading…
Reference in New Issue