-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
server: Factor out authentication-related code into a new namespace, …
…`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
1 parent
4e63943
commit a8d1002
Showing
173 changed files
with
670 additions
and
667 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] |
2 changes: 1 addition & 1 deletion
2
server/src-lib/Hasura/RQL/Types/Roles.hs → server/src-lib/Hasura/Authentication/Role.hs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.