Skip to content

Commit

Permalink
server: Factor out authentication-related code into a new namespace, …
Browse files Browse the repository at this point in the history
…`Hasura.Authentication`.

In preparation for tightening up the various ways in which we construct and work with session variables, I am trying to move the behavior into the same module(s) as the data types, so that we can avoid exposing the internals of data structures in favor of smart constructors and conversions.

The session variable code was split between `Hasura.RQL.Types.Roles`, `Hasura.RQL.Types.Session`, and `Hasura.Session`, with the first two containing most of the data structures (and some logic) and the latter containing the rest of the logic. These files do not interact with the rest of `Hasura.RQL`, though they are depended upon by that namespace.

I have refactored these files into a new namespace, `Hasura.Authentication`. It now looks like this:

1. Role types are now in `Hasura.Authentication.Role`.
2. Header constants were moved from `Hasura.Server.Utils` to `Hasura.Authentication.Headers` (plural) to avoid cycles.
3. Header logic was moved from various places into `Hasura.Authentication.Header` (singular) for the same reason.
4. Session variable types and logic live together in `Hasura.Authentication.Session`.
5. User info types and logic live together in `Hasura.Authentication.User`.

This new structure is cycle-free and generally avoids importing the rest of the code, which means we should be able to start pruning the list of exports and locking down session variable construction.

No behavior was changed in this changeset.

The majority of changes are to the imports in a number of files; everything depends on these things. By splitting into multiple files, we also reduce the surface area of an individual import, which was a pleasant side-effect of this work.

PR-URL: hasura/graphql-engine-mono#10960
GitOrigin-RevId: 7cb962c06483cd9b92b80432aed5cabecb465cda
  • Loading branch information
SamirTalwar authored and hasura-bot committed Jul 29, 2024
1 parent 4e63943 commit a8d1002
Show file tree
Hide file tree
Showing 173 changed files with 670 additions and 667 deletions.
13 changes: 9 additions & 4 deletions server/graphql-engine.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,13 @@ library
, Hasura.Logging
, Hasura.HTTP
, Hasura.PingSources

, Hasura.Authentication.Header
, Hasura.Authentication.Headers
, Hasura.Authentication.Role
, Hasura.Authentication.Session
, Hasura.Authentication.User

, Hasura.Server.API.Backend
, Hasura.Server.API.Instances
, Hasura.Server.API.Metadata
Expand Down Expand Up @@ -722,7 +729,6 @@ library
, Hasura.GraphQL.Logging.QueryLog
, Hasura.GraphQL.Logging.ExecutionLog
, Hasura.RQL.DML.Select
, Hasura.Session

, Hasura.Server.API.Config
, Hasura.Server.Metrics
Expand Down Expand Up @@ -801,6 +807,7 @@ library
, Hasura.RemoteSchema.SchemaCache.RemoteRelationship
, Hasura.RemoteSchema.SchemaCache.Build
, Hasura.RemoteSchema.SchemaCache

, Hasura.RQL.Types.Action
, Hasura.RQL.Types.Allowlist
, Hasura.RQL.Types.ApiLimit
Expand Down Expand Up @@ -838,7 +845,6 @@ library
, Hasura.RQL.Types.Relationships.Remote
, Hasura.RQL.Types.Relationships.ToSource
, Hasura.RQL.Types.ResultCustomization
, Hasura.RQL.Types.Roles
, Hasura.RQL.Types.Roles.Internal
, Hasura.RQL.Types.ResizePool
, Hasura.RQL.Types.ScheduledTrigger
Expand All @@ -848,7 +854,6 @@ library
, Hasura.RQL.Types.SchemaCache.Instances
, Hasura.RQL.Types.SchemaCacheTypes
, Hasura.RQL.Types.Schema.Options
, Hasura.RQL.Types.Session
, Hasura.RQL.Types.Source
, Hasura.RQL.Types.Source.Column
, Hasura.RQL.Types.Source.Table
Expand Down Expand Up @@ -1182,6 +1187,7 @@ test-suite graphql-engine-tests
Data.TimeSpec
Data.TrieSpec
Hasura.AppSpec
Hasura.Authentication.SessionSpec
Hasura.Base.Error.TestInstances
Hasura.Backends.BigQuery.SourceSpec
Hasura.Backends.BigQuery.TypesSpec
Expand Down Expand Up @@ -1248,7 +1254,6 @@ test-suite graphql-engine-tests
Hasura.Server.InitSpec
Hasura.Server.Init.ArgSpec
Hasura.Server.ResourceCheckerSpec
Hasura.SessionSpec
Hasura.SQL.BackendMapSpec
Hasura.SQL.WKTSpec
Hasura.Tracing.TraceIdSpec
Expand Down
2 changes: 1 addition & 1 deletion server/lib/test-harness/src/Harness/Webhook.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import Data.Text.Extended ((<>>))
import Harness.Http qualified as Http
import Harness.Test.TestResource (AcquiredResource (..), Managed, mkTestResource)
import Harness.TestEnvironment (Server (..), serverUrl)
import Hasura.Authentication.Role (RoleName)
import Hasura.Base.Error (iResultToMaybe)
import Hasura.Prelude
import Hasura.RQL.Types.Roles (RoleName)
import Hasura.Server.Utils (executeJSONPath, quoteRegex)
import Network.HTTP.Client.Transformable qualified as HTTP
import Network.Socket qualified as Socket
Expand Down
2 changes: 1 addition & 1 deletion server/src-lib/Data/Aeson/Kriti/Functions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import Data.Aeson qualified as J
import Data.Environment qualified as Env
import Data.HashMap.Strict qualified as HashMap
import Data.Text qualified as T
import Hasura.Authentication.Session (SessionVariables, getSessionVariableValue, mkSessionVariable)
import Hasura.Prelude
import Hasura.Session (SessionVariables, getSessionVariableValue, mkSessionVariable)
import Kriti qualified
import Kriti.CustomFunctions qualified as Kriti
import Kriti.Error (SerializeError (serialize), SerializedError)
Expand Down
4 changes: 2 additions & 2 deletions server/src-lib/Hasura/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import Database.PG.Query qualified as PG
import Database.PG.Query qualified as Q
import GHC.AssertNF.CPP
import Hasura.App.State
import Hasura.Authentication.Role (adminRoleName)
import Hasura.Authentication.User (ExtraUserInfo (..), UserInfo (..))
import Hasura.Backends.MSSQL.Connection
import Hasura.Backends.Postgres.Connection
import Hasura.Base.Error
Expand Down Expand Up @@ -131,7 +133,6 @@ import Hasura.RQL.Types.BackendType
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Metadata
import Hasura.RQL.Types.ResizePool
import Hasura.RQL.Types.Roles (adminRoleName)
import Hasura.RQL.Types.SchemaCache
import Hasura.RQL.Types.SchemaCache.Build
import Hasura.RQL.Types.Source
Expand All @@ -157,7 +158,6 @@ import Hasura.Server.Telemetry
import Hasura.Server.Types
import Hasura.Server.Version
import Hasura.Services
import Hasura.Session
import Hasura.ShutdownLatch
import Hasura.Tracing
import Network.HTTP.Client qualified as HTTP
Expand Down
2 changes: 1 addition & 1 deletion server/src-lib/Hasura/App/State.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Environment qualified as E
import Data.HashSet qualified as Set
import Database.PG.Query qualified as PG
import Hasura.Authentication.Role (RoleName)
import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey)
import Hasura.Base.Error
import Hasura.CredentialCache
Expand All @@ -44,7 +45,6 @@ import Hasura.RQL.Types.BackendType
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Metadata
import Hasura.RQL.Types.NamingCase
import Hasura.RQL.Types.Roles (RoleName)
import Hasura.RQL.Types.Schema.Options qualified as Options
import Hasura.RQL.Types.SchemaCache (MetadataResourceVersion)
import Hasura.Server.Auth
Expand Down
32 changes: 32 additions & 0 deletions server/src-lib/Hasura/Authentication/Header.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Hasura.Authentication.Header
( filterHeaders,
getRequestHeader,
mkSetCookieHeaders,
redactSensitiveHeader,
)
where

import Control.Lens
import Data.ByteString (ByteString)
import Data.HashSet qualified as HashSet
import Hasura.Authentication.Headers
import Hasura.Prelude
import Network.HTTP.Types qualified as HTTP
import Network.Wreq qualified as Wreq

getRequestHeader :: HTTP.HeaderName -> [HTTP.Header] -> Maybe ByteString
getRequestHeader hdrName hdrs = snd <$> mHeader
where
mHeader = find (\h -> fst h == hdrName) hdrs

filterHeaders :: HashSet HTTP.HeaderName -> [HTTP.Header] -> [HTTP.Header]
filterHeaders list = filter (\(n, _) -> not $ n `HashSet.member` list)

redactSensitiveHeader :: HTTP.Header -> HTTP.Header
redactSensitiveHeader (headerName, value) = (headerName, if headerName `elem` sensitiveHeaders then "<REDACTED>" else value)

mkSetCookieHeaders :: Wreq.Response a -> HTTP.ResponseHeaders
mkSetCookieHeaders resp =
map (headerName,) $ resp ^.. Wreq.responseHeader headerName
where
headerName = "Set-Cookie"
76 changes: 76 additions & 0 deletions server/src-lib/Hasura/Authentication/Headers.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module Hasura.Authentication.Headers
( adminSecretHeader,
contentLengthHeader,
deprecatedAccessKeyHeader,
gzipHeader,
jsonHeader,
requestIdHeader,
sqlHeader,
useBackendOnlyPermissionsHeader,
userIdHeader,
userRoleHeader,
sensitiveHeaders,
commonClientHeadersIgnored,
)
where

import Data.HashSet qualified as HashSet
import Hasura.Prelude
import Network.HTTP.Types qualified as HTTP

jsonHeader :: HTTP.Header
jsonHeader = ("Content-Type", "application/json; charset=utf-8")

sqlHeader :: HTTP.Header
sqlHeader = ("Content-Type", "application/sql; charset=utf-8")

gzipHeader :: HTTP.Header
gzipHeader = ("Content-Encoding", "gzip")

userRoleHeader :: (IsString a) => a
userRoleHeader = "x-hasura-role"

deprecatedAccessKeyHeader :: (IsString a) => a
deprecatedAccessKeyHeader = "x-hasura-access-key"

adminSecretHeader :: (IsString a) => a
adminSecretHeader = "x-hasura-admin-secret"

userIdHeader :: (IsString a) => a
userIdHeader = "x-hasura-user-id"

requestIdHeader :: (IsString a) => a
requestIdHeader = "x-request-id"

contentLengthHeader :: (IsString a) => a
contentLengthHeader = "Content-Length"

useBackendOnlyPermissionsHeader :: (IsString a) => a
useBackendOnlyPermissionsHeader = "x-hasura-use-backend-only-permissions"

sensitiveHeaders :: HashSet HTTP.HeaderName
sensitiveHeaders =
HashSet.fromList
[ "Access-Token",
"Authorization",
"Cookie"
]

-- ignore the following request headers from the client
commonClientHeadersIgnored :: (IsString a) => [a]
commonClientHeadersIgnored =
[ "Content-Length",
"Content-MD5",
"User-Agent",
"Host",
"Origin",
"Referer",
"Accept",
"Accept-Encoding",
"Accept-Language",
"Accept-Datetime",
"Cache-Control",
"Connection",
"DNT",
"Content-Type"
]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Hasura.RQL.Types.Roles
module Hasura.Authentication.Role
( DropInheritedRole (..),
InheritedRole,
ParentRoles (..),
Expand Down
154 changes: 154 additions & 0 deletions server/src-lib/Hasura/Authentication/Session.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
module Hasura.Authentication.Session
( SessionVariable,
SessionVariables (..),
SessionVariableValue,
parseSessionVariable,
sessionVariableToText,
mkSessionVariable,
mkSessionVariablesText,
isSessionVariable,
filterSessionVariables,
sessionVariableToGraphQLName,
sessionVariablesToHeaders,
mkSessionVariablesHeaders,
getSessionVariableValue,
getSessionVariablesSet,
getSessionVariables,
maybeRoleFromSessionVariables,
mkClientHeadersForward,
)
where

import Data.Aeson
import Data.Aeson.Types (Parser, toJSONKeyText)
import Data.CaseInsensitive qualified as CI
import Data.HashMap.Strict qualified as HashMap
import Data.HashSet qualified as HashSet
import Data.Text qualified as T
import Data.Text.Extended
import Hasura.Authentication.Header (filterHeaders)
import Hasura.Authentication.Headers (commonClientHeadersIgnored, userRoleHeader)
import Hasura.Authentication.Role (RoleName, mkRoleName)
import Hasura.Prelude
import Language.GraphQL.Draft.Syntax qualified as G
import Network.HTTP.Types qualified as HTTP

newtype SessionVariable = SessionVariable {unSessionVariable :: CI.CI Text}
deriving (Show, Eq, Hashable, IsString, Data, NFData, Ord)

instance ToJSON SessionVariable where
toJSON = toJSON . CI.original . unSessionVariable

instance ToJSONKey SessionVariable where
toJSONKey = toJSONKeyText sessionVariableToText

instance ToTxt SessionVariable where
toTxt = sessionVariableToText

type SessionVariableValue = Text

sessionVariablePrefix :: Text
sessionVariablePrefix = "x-hasura-"

isSessionVariable :: Text -> Bool
{-# INLINE isSessionVariable #-} -- hope any redundant conversions vis a vis SessionVariable are eliminated
isSessionVariable = T.isPrefixOf sessionVariablePrefix . T.toCaseFold

-- | A more efficient form of 'isSessionVariable', where applicable
isSessionVariableCI :: CI.CI Text -> Bool
{-# INLINE isSessionVariableCI #-}
isSessionVariableCI = T.isPrefixOf sessionVariablePrefix . CI.foldedCase

parseSessionVariable :: Text -> Parser SessionVariable
parseSessionVariable t =
-- for performance we avoid isSessionVariable, doing just one case conversion
let sessionVar_dirty = mkSessionVariable t
in if sessionVariablePrefix `T.isPrefixOf` CI.foldedCase (unSessionVariable sessionVar_dirty)
then pure sessionVar_dirty
else fail $ show t <> " is not a Hasura session variable"

instance FromJSON SessionVariable where
parseJSON = withText "String" parseSessionVariable

instance FromJSONKey SessionVariable where
fromJSONKey = FromJSONKeyTextParser parseSessionVariable

-- | in normalized, lower-case form
sessionVariableToText :: SessionVariable -> Text
sessionVariableToText = CI.foldedCase . unSessionVariable

mkSessionVariable :: Text -> SessionVariable
mkSessionVariable = SessionVariable . CI.mk

newtype SessionVariables = SessionVariables {unSessionVariables :: HashMap.HashMap SessionVariable SessionVariableValue}
deriving (Show, Eq, Hashable, Semigroup, Monoid)

instance ToJSON SessionVariables where
toJSON (SessionVariables varMap) =
toJSON $ mapKeys sessionVariableToText varMap

instance FromJSON SessionVariables where
parseJSON v = mkSessionVariablesText <$> parseJSON v

mkSessionVariablesText :: HashMap.HashMap Text Text -> SessionVariables
mkSessionVariablesText = SessionVariables . mapKeys mkSessionVariable

-- | Converts a `SessionVariable` value to a GraphQL name.
-- This will fail if the session variable contains characters that are not valid
-- for a graphql names. It is the caller's responsibility to decide what to do
-- in such a case.
sessionVariableToGraphQLName :: SessionVariable -> Maybe G.Name
sessionVariableToGraphQLName = G.mkName . T.replace "-" "_" . sessionVariableToText

filterSessionVariables ::
(SessionVariable -> SessionVariableValue -> Bool) ->
SessionVariables ->
SessionVariables
filterSessionVariables f = SessionVariables . HashMap.filterWithKey f . unSessionVariables

mkSessionVariablesHeaders :: [HTTP.Header] -> SessionVariables
mkSessionVariablesHeaders =
SessionVariables
. HashMap.fromList
. map (first SessionVariable)
. filter (isSessionVariableCI . fst) -- Only x-hasura-* headers
. map (CI.map bsToTxt *** bsToTxt)

---- Something like this a little faster, but I expect some test failures
-- . map (lowerToTxt *** bsToTxt)
-- where
-- -- NOTE: this throws away the original, vs 'CI.map bsToTxt'
-- lowerToTxt = CI.unsafeMk . bsToTxt . CI.foldedCase

sessionVariablesToHeaders :: SessionVariables -> [HTTP.Header]
sessionVariablesToHeaders =
map ((CI.map txtToBs . unSessionVariable) *** txtToBs)
. HashMap.toList
. unSessionVariables

getSessionVariables :: SessionVariables -> [Text]
getSessionVariables = map sessionVariableToText . HashMap.keys . unSessionVariables

getSessionVariablesSet :: SessionVariables -> HashSet SessionVariable
getSessionVariablesSet = HashMap.keysSet . unSessionVariables

getSessionVariableValue :: SessionVariable -> SessionVariables -> Maybe SessionVariableValue
getSessionVariableValue k = HashMap.lookup k . unSessionVariables

maybeRoleFromSessionVariables :: SessionVariables -> Maybe RoleName
maybeRoleFromSessionVariables sessionVariables =
-- returns Nothing if x-hasura-role is an empty string
getSessionVariableValue userRoleHeader sessionVariables >>= mkRoleName

mkClientHeadersForward :: [HTTP.Header] -> [HTTP.Header]
mkClientHeadersForward reqHeaders =
xForwardedHeaders <> (filterVars . filterRequestHeaders) reqHeaders
where
filterRequestHeaders = filterHeaders $ HashSet.fromList commonClientHeadersIgnored
filterVars = filter (\(k, _) -> not $ isSessionVariable $ bsToTxt $ CI.original k)
xForwardedHeaders = flip mapMaybe reqHeaders $ \(hdrName, hdrValue) ->
case hdrName of
"Host" -> Just ("X-Forwarded-Host", hdrValue)
"User-Agent" -> Just ("X-Forwarded-User-Agent", hdrValue)
"Origin" -> Just ("X-Forwarded-Origin", hdrValue)
_ -> Nothing
Loading

0 comments on commit a8d1002

Please sign in to comment.