Compare commits

..

4 Commits

Author SHA1 Message Date
lukaszraczylo 2a0302ab75 Create allow list for event when intospection is blocked but developers
really want to use certain subqueries.
2023-10-15 10:01:23 +01:00
lukaszraczylo 29ffb8a817 Update README.md 2023-10-14 08:58:55 +01:00
lukaszraczylo 6ac3937066 Fix leaky bytes allocation for cache. 2023-10-13 16:29:52 +01:00
lukaszraczylo 089d05b7c3 Improve cache mechanism using sync map 2023-10-13 15:37:57 +01:00
8 changed files with 83 additions and 39 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ help: ## display this help
.PHONY: run
run: build ## run application
@LOG_LEVEL=debug BLOCK_SCHEMA_INTROSPECTION=false JWT_ROLE_RATE_LIMIT=false JWT_ROLE_CLAIM_PATH="Hasura.x-hasura-default-role" JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/ ./graphql-proxy
@LOG_LEVEL=debug BLOCK_SCHEMA_INTROSPECTION=false CACHE_TTL=10 JWT_ROLE_RATE_LIMIT=false JWT_ROLE_CLAIM_PATH="Hasura.x-hasura-default-role" JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/ ./graphql-proxy
.PHONY: build
build: ## build the binary
+12 -2
View File
@@ -42,12 +42,13 @@ I wanted to monitor the queries and responses of our graphql endpoint. Still, we
| `HOST_GRAPHQL` | The host to proxy the graphql endpoint | `http://localhost/` |
| `JWT_USER_CLAIM_PATH` | Path to the user claim in the JWT token | `` |
| `JWT_ROLE_CLAIM_PATH` | Path to the role claim in the JWT token | `` |
| `JWT_ROLE_FROM_HEADER` | Header name to extract the role from | `` |
| `ROLE_FROM_HEADER` | Header name to extract the role from | `` |
| `ROLE_RATE_LIMIT` | Enable request rate limiting based on role| `false` |
| `ENABLE_GLOBAL_CACHE` | Enable the cache | `false` |
| `CACHE_TTL` | The cache TTL | `60` |
| `LOG_LEVEL` | The log level | `info` |
| `BLOCK_SCHEMA_INTROSPECTION`| Blocks the schema introspection | `false` |
| `ALLOWED_INTROSPECTION` | Allow only certain queries in introspection | `` |
| `ENABLE_ACCESS_LOG` | Enable the access log | `false` |
| `READ_ONLY_MODE` | Enable the read only mode | `false` |
| `ALLOWED_URLS` | Allow access only to certain URLs | `/v1/graphql,/v1/version` |
@@ -107,6 +108,15 @@ You can enable the read-only mode by setting the `READ_ONLY_MODE` environment va
You can allow access only to certain URLs by setting the `ALLOWED_URLS` environment variable to a comma-separated list of URLs. If enabled - other URLs will return `403 Forbidden` error and request will **not** reach the proxied service.
### Blocking introspection
You can block the schema introspection by setting the `BLOCK_SCHEMA_INTROSPECTION` environment variable to `true` - which will block all the queries with introspection parts, like:
`__schema`, `__type`, `__typename`, `__directive`, `__directivelocation`, `__field`, `__inputvalue`, `__enumvalue`, `__typekind`, `__fieldtype`, `__inputobjecttype`, `__enumtype`, `__uniontype`, `__scalars`, `__objects`, `__interfaces`, `__unions`, `__enums`, `__inputobjects`, `__directives`
If you'd like to keep blocking of the schema introspection on but allow one or more of from the list of above for any reason, you can use the `ALLOWED_INTROSPECTION` environment variable to specify the list of allowed queries.
`ALLOWED_INTROSPECTION="__typename,__type"`
### Monitoring endpoint
@@ -125,4 +135,4 @@ graphql_proxy_executed_query{user_id="-",op_type="query",op_name="checkIfSpamAIR
graphql_proxy_requests_failed 324
graphql_proxy_requests_skipped 0
graphql_proxy_requests_succesful 454823
```
```
+4 -6
View File
@@ -14,15 +14,13 @@ func calculateHash(c *fiber.Ctx) string {
}
func enableCache() {
cfg.Cache.CacheClient = libpack_cache.New(time.Duration(cfg.Cache.CacheTTL) * time.Second * 2)
cfg.Cache.CacheClient = libpack_cache.New(time.Duration(cfg.Cache.CacheTTL) * time.Second * 100)
}
func cacheLookup(hash string) []byte {
if cfg.Cache.CacheClient != nil {
obj, found := cfg.Cache.CacheClient.Get(hash)
if found {
return obj
}
obj, found := cfg.Cache.CacheClient.Get(hash)
if found {
return obj
}
return nil
}
+46 -26
View File
@@ -12,16 +12,21 @@ type CacheEntry struct {
type Cache struct {
sync.RWMutex
entries map[string]CacheEntry
entries sync.Map
globalTTL time.Duration
bytePool sync.Pool
}
func New(globalTTL time.Duration) *Cache {
cache := &Cache{
entries: make(map[string]CacheEntry),
globalTTL: globalTTL,
}
// Initialize the byte pool.
cache.bytePool.New = func() interface{} {
return make([]byte, 0)
}
// Start the cache cleanup.
go cache.cleanupRoutine(globalTTL)
return cache
@@ -39,54 +44,69 @@ func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
c.Lock()
defer c.Unlock()
expiresAt := time.Now().Add(ttl)
now := time.Now()
expiresAt := now.Add(ttl)
if expiresAt.After(now.Add(c.globalTTL)) {
expiresAt = now.Add(c.globalTTL)
// Get a byte slice from the pool and ensure it's properly sized.
b := c.bytePool.Get().([]byte)
if cap(b) < len(value) {
b = make([]byte, len(value))
} else {
b = b[:len(value)]
}
c.entries[key] = CacheEntry{
Value: value,
copy(b, value)
entry := CacheEntry{
Value: b,
ExpiresAt: expiresAt,
}
c.entries.Store(key, entry)
}
func (c *Cache) Get(key string) ([]byte, bool) {
c.RLock()
defer c.RUnlock()
entry, ok := c.entries[key]
if !ok || entry.ExpiresAt.Before(time.Now()) {
entry, ok := c.entries.Load(key)
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
return nil, false
}
return entry.Value, true
// Copy the value from the byte slice.
value := make([]byte, len(entry.(CacheEntry).Value))
copy(value, entry.(CacheEntry).Value)
return value, true
}
func (c *Cache) Delete(key string) {
c.Lock()
defer c.Unlock()
delete(c.entries, key)
entry, ok := c.entries.Load(key)
if !ok {
return
}
// Return the byte slice to the pool.
c.bytePool.Put(entry.(CacheEntry).Value)
// Delete the entry from the cache.
c.entries.Delete(key)
}
func (c *Cache) CleanExpiredEntries() {
now := time.Now()
toDelete := make([]string, 0)
c.RLock()
for key, entry := range c.entries {
c.entries.Range(func(key, value interface{}) bool {
entry := value.(CacheEntry)
if entry.ExpiresAt.Before(now) {
toDelete = append(toDelete, key)
}
}
c.RUnlock()
// Return the byte slice to the pool.
c.bytePool.Put(entry.Value)
// Separate the deletion to its own critical section to reduce lock contention.
c.Lock()
for _, key := range toDelete {
delete(c.entries, key)
}
c.Unlock()
// Delete the entry from the cache.
c.entries.Delete(key)
}
// Return true to continue iterating over the map.
return true
})
}
+2 -2
View File
@@ -27,7 +27,7 @@ func (suite *Tests) Test_cacheLookup() {
{
name: "test_existent",
args: args{
hash: "00000000000000000000000000000000000001",
hash: "00000000000000000000000000000000001337",
},
want: []byte("it's fine."),
addCache: struct {
@@ -40,7 +40,7 @@ func (suite *Tests) Test_cacheLookup() {
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
if tt.addCache.data != nil {
cfg.Cache.CacheClient.Set(tt.args.hash, tt.addCache.data, time.Duration(1)*time.Second)
cfg.Cache.CacheClient.Set(tt.args.hash, tt.addCache.data, time.Duration(90*time.Second))
}
got := cacheLookup(tt.args.hash)
assert.Equal(tt.want, got, "Unexpected cache lookup result")
+9 -1
View File
@@ -96,7 +96,15 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
if cfg.Security.BlockIntrospection {
for _, s := range oper.SelectionSet.Selections {
for _, s2 := range s.GetSelectionSet().Selections {
if _, exists := retrospectionQuerySet[s2.(*ast.Field).Name.Value]; exists {
if _, exists := retrospectionQuerySet[strings.ToLower(s2.(*ast.Field).Name.Value)]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
for _, introspectionQueryAllowed := range cfg.Security.IntrospectionAllowed {
if strings.EqualFold(strings.ToLower(introspectionQueryAllowed), strings.ToLower(s2.(*ast.Field).Name.Value)) {
cfg.Logger.Debug("Introspection query allowed, passing through", m)
return
}
}
}
cfg.Logger.Warning("Introspection query blocked", m)
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
c.Status(403).SendString("Introspection queries are not allowed")
+7
View File
@@ -30,6 +30,13 @@ func parseConfig() {
c.Cache.CacheEnable = envutil.GetBool("ENABLE_GLOBAL_CACHE", false)
c.Cache.CacheTTL = envutil.GetInt("CACHE_TTL", 60)
c.Security.BlockIntrospection = envutil.GetBool("BLOCK_SCHEMA_INTROSPECTION", false)
c.Security.IntrospectionAllowed = func() []string {
urls := envutil.Getenv("ALLOWED_INTROSPECTION", "")
if urls == "" {
return nil
}
return strings.Split(urls, ",")
}()
c.Logger = libpack_logging.NewLogger()
c.Client.GQLClient = graphql.NewConnection()
c.Client.GQLClient.SetEndpoint(c.Server.HostGraphQL)
+2 -1
View File
@@ -40,6 +40,7 @@ type config struct {
}
Security struct {
BlockIntrospection bool
BlockIntrospection bool
IntrospectionAllowed []string
}
}