Add Credentials module to use Assume Role API (#184)

This exports a new module for retrieving STS based credentials, however
they are not yet convenient to use in the library - the session token
needs to be included as a custom header and may not be possible with all
APIs.
This commit is contained in:
Aditya Manthramurthy 2022-12-23 07:53:27 -08:00 committed by GitHub
parent d87d67b75b
commit f4ae55468e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 663 additions and 176 deletions

View File

@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: mrkkrp/ormolu-action@v6 - uses: mrkkrp/ormolu-action@v8
hlint: hlint:
runs-on: ubuntu-latest runs-on: ubuntu-latest

33
examples/AssumeRole.hs Normal file
View File

@ -0,0 +1,33 @@
--
-- MinIO Haskell SDK, (C) 2022 MinIO, Inc.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
{-# LANGUAGE OverloadedStrings #-}
import Network.Minio.Credentials
import Prelude
main :: IO ()
main = do
res <-
retrieveCredentials
$ STSAssumeRole
"https://play.min.io"
( CredentialValue
"Q3AM3UQ867SPQQA43P2F"
"zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
Nothing
)
$ defaultSTSAssumeRoleOptions {saroLocation = Just "us-east-1"}
print res

View File

@ -128,6 +128,7 @@ common base-settings
, retry , retry
, text >= 1.2 , text >= 1.2
, time >= 1.9 , time >= 1.9
, time-units ^>= 1.0.0
, transformers >= 0.5 , transformers >= 0.5
, unliftio >= 0.2 && < 0.3 , unliftio >= 0.2 && < 0.3
, unliftio-core >= 0.2 && < 0.3 , unliftio-core >= 0.2 && < 0.3
@ -140,6 +141,7 @@ library
exposed-modules: Network.Minio exposed-modules: Network.Minio
, Network.Minio.AdminAPI , Network.Minio.AdminAPI
, Network.Minio.S3API , Network.Minio.S3API
, Network.Minio.Credentials
Flag live-test Flag live-test
Description: Build the test suite that runs against a live MinIO server Description: Build the test suite that runs against a live MinIO server
@ -339,3 +341,8 @@ executable SetConfig
import: examples-settings import: examples-settings
scope: private scope: private
main-is: SetConfig.hs main-is: SetConfig.hs
executable AssumeRole
import: examples-settings
scope: private
main-is: AssumeRole.hs

View File

@ -34,6 +34,7 @@ import Control.Retry
limitRetriesByCumulativeDelay, limitRetriesByCumulativeDelay,
retrying, retrying,
) )
import qualified Data.ByteArray as BA
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.Char as C import qualified Data.Char as C
import qualified Data.Conduit as C import qualified Data.Conduit as C
@ -44,6 +45,7 @@ import Lib.Prelude
import qualified Network.HTTP.Client as NClient import qualified Network.HTTP.Client as NClient
import Network.HTTP.Conduit (Response) import Network.HTTP.Conduit (Response)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (simpleQueryToQuery)
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
import Network.HTTP.Types.Header (hHost) import Network.HTTP.Types.Header (hHost)
import Network.Minio.APICommon import Network.Minio.APICommon
@ -176,7 +178,8 @@ buildRequest ri = do
let sp = let sp =
SignParams SignParams
(connectAccessKey ci') (connectAccessKey ci')
(connectSecretKey ci') (BA.convert (encodeUtf8 $ connectSecretKey ci' :: ByteString))
ServiceS3
timeStamp timeStamp
(riRegion ri') (riRegion ri')
(riPresignExpirySecs ri') (riPresignExpirySecs ri')
@ -198,8 +201,8 @@ buildRequest ri = do
| isJust (riPresignExpirySecs ri') -> | isJust (riPresignExpirySecs ri') ->
-- case 0 from above. -- case 0 from above.
do do
let signPairs = signV4 sp baseRequest let signPairs = signV4QueryParams sp baseRequest
qpToAdd = (fmap . fmap) Just signPairs qpToAdd = simpleQueryToQuery signPairs
existingQueryParams = HT.parseQuery (NC.queryString baseRequest) existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
updatedQueryParams = existingQueryParams ++ qpToAdd updatedQueryParams = existingQueryParams ++ qpToAdd
return $ NClient.setQueryString updatedQueryParams baseRequest return $ NClient.setQueryString updatedQueryParams baseRequest
@ -229,8 +232,7 @@ buildRequest ri = do
return $ return $
baseRequest baseRequest
{ NC.requestHeaders = { NC.requestHeaders =
NC.requestHeaders baseRequest NC.requestHeaders baseRequest ++ signHeaders,
++ mkHeaderFromPairs signHeaders,
NC.requestBody = getRequestBody (riPayload ri') NC.requestBody = getRequestBody (riPayload ri')
} }

View File

@ -70,6 +70,7 @@ import Data.Aeson
) )
import qualified Data.Aeson as A import qualified Data.Aeson as A
import Data.Aeson.Types (typeMismatch) import Data.Aeson.Types (typeMismatch)
import qualified Data.ByteArray as BA
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T import qualified Data.Text as T
@ -95,9 +96,12 @@ data DriveInfo = DriveInfo
instance FromJSON DriveInfo where instance FromJSON DriveInfo where
parseJSON = withObject "DriveInfo" $ \v -> parseJSON = withObject "DriveInfo" $ \v ->
DriveInfo DriveInfo
<$> v .: "uuid" <$> v
<*> v .: "endpoint" .: "uuid"
<*> v .: "state" <*> v
.: "endpoint"
<*> v
.: "state"
data StorageClass = StorageClass data StorageClass = StorageClass
{ scParity :: Int, { scParity :: Int,
@ -120,12 +124,16 @@ instance FromJSON ErasureInfo where
offlineDisks <- v .: "OfflineDisks" offlineDisks <- v .: "OfflineDisks"
stdClass <- stdClass <-
StorageClass StorageClass
<$> v .: "StandardSCData" <$> v
<*> v .: "StandardSCParity" .: "StandardSCData"
<*> v
.: "StandardSCParity"
rrClass <- rrClass <-
StorageClass StorageClass
<$> v .: "RRSCData" <$> v
<*> v .: "RRSCParity" .: "RRSCData"
<*> v
.: "RRSCParity"
sets <- v .: "Sets" sets <- v .: "Sets"
return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
@ -151,8 +159,10 @@ data ConnStats = ConnStats
instance FromJSON ConnStats where instance FromJSON ConnStats where
parseJSON = withObject "ConnStats" $ \v -> parseJSON = withObject "ConnStats" $ \v ->
ConnStats ConnStats
<$> v .: "transferred" <$> v
<*> v .: "received" .: "transferred"
<*> v
.: "received"
data ServerProps = ServerProps data ServerProps = ServerProps
{ spUptime :: NominalDiffTime, { spUptime :: NominalDiffTime,
@ -182,8 +192,10 @@ data StorageInfo = StorageInfo
instance FromJSON StorageInfo where instance FromJSON StorageInfo where
parseJSON = withObject "StorageInfo" $ \v -> parseJSON = withObject "StorageInfo" $ \v ->
StorageInfo StorageInfo
<$> v .: "Used" <$> v
<*> v .: "Backend" .: "Used"
<*> v
.: "Backend"
data CountNAvgTime = CountNAvgTime data CountNAvgTime = CountNAvgTime
{ caCount :: Int64, { caCount :: Int64,
@ -194,8 +206,10 @@ data CountNAvgTime = CountNAvgTime
instance FromJSON CountNAvgTime where instance FromJSON CountNAvgTime where
parseJSON = withObject "CountNAvgTime" $ \v -> parseJSON = withObject "CountNAvgTime" $ \v ->
CountNAvgTime CountNAvgTime
<$> v .: "count" <$> v
<*> v .: "avgDuration" .: "count"
<*> v
.: "avgDuration"
data HttpStats = HttpStats data HttpStats = HttpStats
{ hsTotalHeads :: CountNAvgTime, { hsTotalHeads :: CountNAvgTime,
@ -214,16 +228,26 @@ data HttpStats = HttpStats
instance FromJSON HttpStats where instance FromJSON HttpStats where
parseJSON = withObject "HttpStats" $ \v -> parseJSON = withObject "HttpStats" $ \v ->
HttpStats HttpStats
<$> v .: "totalHEADs" <$> v
<*> v .: "successHEADs" .: "totalHEADs"
<*> v .: "totalGETs" <*> v
<*> v .: "successGETs" .: "successHEADs"
<*> v .: "totalPUTs" <*> v
<*> v .: "successPUTs" .: "totalGETs"
<*> v .: "totalPOSTs" <*> v
<*> v .: "successPOSTs" .: "successGETs"
<*> v .: "totalDELETEs" <*> v
<*> v .: "successDELETEs" .: "totalPUTs"
<*> v
.: "successPUTs"
<*> v
.: "totalPOSTs"
<*> v
.: "successPOSTs"
<*> v
.: "totalDELETEs"
<*> v
.: "successDELETEs"
data SIData = SIData data SIData = SIData
{ sdStorage :: StorageInfo, { sdStorage :: StorageInfo,
@ -236,10 +260,14 @@ data SIData = SIData
instance FromJSON SIData where instance FromJSON SIData where
parseJSON = withObject "SIData" $ \v -> parseJSON = withObject "SIData" $ \v ->
SIData SIData
<$> v .: "storage" <$> v
<*> v .: "network" .: "storage"
<*> v .: "http" <*> v
<*> v .: "server" .: "network"
<*> v
.: "http"
<*> v
.: "server"
data ServerInfo = ServerInfo data ServerInfo = ServerInfo
{ siError :: Text, { siError :: Text,
@ -251,9 +279,12 @@ data ServerInfo = ServerInfo
instance FromJSON ServerInfo where instance FromJSON ServerInfo where
parseJSON = withObject "ServerInfo" $ \v -> parseJSON = withObject "ServerInfo" $ \v ->
ServerInfo ServerInfo
<$> v .: "error" <$> v
<*> v .: "addr" .: "error"
<*> v .: "data" <*> v
.: "addr"
<*> v
.: "data"
data ServerVersion = ServerVersion data ServerVersion = ServerVersion
{ svVersion :: Text, { svVersion :: Text,
@ -264,8 +295,10 @@ data ServerVersion = ServerVersion
instance FromJSON ServerVersion where instance FromJSON ServerVersion where
parseJSON = withObject "ServerVersion" $ \v -> parseJSON = withObject "ServerVersion" $ \v ->
ServerVersion ServerVersion
<$> v .: "version" <$> v
<*> v .: "commitID" .: "version"
<*> v
.: "commitID"
data ServiceStatus = ServiceStatus data ServiceStatus = ServiceStatus
{ ssVersion :: ServerVersion, { ssVersion :: ServerVersion,
@ -306,9 +339,12 @@ data HealStartResp = HealStartResp
instance FromJSON HealStartResp where instance FromJSON HealStartResp where
parseJSON = withObject "HealStartResp" $ \v -> parseJSON = withObject "HealStartResp" $ \v ->
HealStartResp HealStartResp
<$> v .: "clientToken" <$> v
<*> v .: "clientAddress" .: "clientToken"
<*> v .: "startTime" <*> v
.: "clientAddress"
<*> v
.: "startTime"
data HealOpts = HealOpts data HealOpts = HealOpts
{ hoRecursive :: Bool, { hoRecursive :: Bool,
@ -325,8 +361,10 @@ instance ToJSON HealOpts where
instance FromJSON HealOpts where instance FromJSON HealOpts where
parseJSON = withObject "HealOpts" $ \v -> parseJSON = withObject "HealOpts" $ \v ->
HealOpts HealOpts
<$> v .: "recursive" <$> v
<*> v .: "dryRun" .: "recursive"
<*> v
.: "dryRun"
data HealItemType data HealItemType
= HealItemMetadata = HealItemMetadata
@ -353,9 +391,12 @@ data NodeSummary = NodeSummary
instance FromJSON NodeSummary where instance FromJSON NodeSummary where
parseJSON = withObject "NodeSummary" $ \v -> parseJSON = withObject "NodeSummary" $ \v ->
NodeSummary NodeSummary
<$> v .: "name" <$> v
<*> v .: "errSet" .: "name"
<*> v .: "errMsg" <*> v
.: "errSet"
<*> v
.: "errMsg"
data SetConfigResult = SetConfigResult data SetConfigResult = SetConfigResult
{ scrStatus :: Bool, { scrStatus :: Bool,
@ -366,8 +407,10 @@ data SetConfigResult = SetConfigResult
instance FromJSON SetConfigResult where instance FromJSON SetConfigResult where
parseJSON = withObject "SetConfigResult" $ \v -> parseJSON = withObject "SetConfigResult" $ \v ->
SetConfigResult SetConfigResult
<$> v .: "status" <$> v
<*> v .: "nodeResults" .: "status"
<*> v
.: "nodeResults"
data HealResultItem = HealResultItem data HealResultItem = HealResultItem
{ hriResultIdx :: Int, { hriResultIdx :: Int,
@ -388,16 +431,26 @@ data HealResultItem = HealResultItem
instance FromJSON HealResultItem where instance FromJSON HealResultItem where
parseJSON = withObject "HealResultItem" $ \v -> parseJSON = withObject "HealResultItem" $ \v ->
HealResultItem HealResultItem
<$> v .: "resultId" <$> v
<*> v .: "type" .: "resultId"
<*> v .: "bucket" <*> v
<*> v .: "object" .: "type"
<*> v .: "detail" <*> v
<*> v .:? "parityBlocks" .: "bucket"
<*> v .:? "dataBlocks" <*> v
<*> v .: "diskCount" .: "object"
<*> v .: "setCount" <*> v
<*> v .: "objectSize" .: "detail"
<*> v
.:? "parityBlocks"
<*> v
.:? "dataBlocks"
<*> v
.: "diskCount"
<*> v
.: "setCount"
<*> v
.: "objectSize"
<*> ( do <*> ( do
before <- v .: "before" before <- v .: "before"
before .: "drives" before .: "drives"
@ -420,12 +473,18 @@ data HealStatus = HealStatus
instance FromJSON HealStatus where instance FromJSON HealStatus where
parseJSON = withObject "HealStatus" $ \v -> parseJSON = withObject "HealStatus" $ \v ->
HealStatus HealStatus
<$> v .: "Summary" <$> v
<*> v .: "StartTime" .: "Summary"
<*> v .: "Settings" <*> v
<*> v .: "NumDisks" .: "StartTime"
<*> v .:? "Detail" <*> v
<*> v .: "Items" .: "Settings"
<*> v
.: "NumDisks"
<*> v
.:? "Detail"
<*> v
.: "Items"
healPath :: Maybe Bucket -> Maybe Text -> ByteString healPath :: Maybe Bucket -> Maybe Text -> ByteString
healPath bucket prefix = do healPath bucket prefix = do
@ -620,7 +679,8 @@ buildAdminRequest areq = do
sp = sp =
SignParams SignParams
(connectAccessKey ci) (connectAccessKey ci)
(connectSecretKey ci) (BA.convert (encodeUtf8 $ connectSecretKey ci :: ByteString))
ServiceS3
timeStamp timeStamp
Nothing Nothing
Nothing Nothing
@ -630,7 +690,7 @@ buildAdminRequest areq = do
-- Update signReq with Authorization header containing v4 signature -- Update signReq with Authorization header containing v4 signature
return return
signReq signReq
{ NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
} }
where where
toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request

View File

@ -0,0 +1,144 @@
--
-- MinIO Haskell SDK, (C) 2017-2022 MinIO, Inc.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
module Network.Minio.Credentials
( CredentialValue (..),
CredentialProvider (..),
AccessKey,
SecretKey,
SessionToken,
defaultSTSAssumeRoleOptions,
STSAssumeRole (..),
STSAssumeRoleOptions (..),
)
where
import qualified Data.Time as Time
import Data.Time.Units (Second)
import Network.HTTP.Client (RequestBody (RequestBodyBS))
import qualified Network.HTTP.Client as NC
import qualified Network.HTTP.Client.TLS as NC
import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
import Network.HTTP.Types.Header (hHost)
import Network.Minio.Data
import Network.Minio.Data.Crypto (hashSHA256)
import Network.Minio.Sign.V4
import Network.Minio.Utils (httpLbs)
import Network.Minio.XmlParser (parseSTSAssumeRoleResult)
class CredentialProvider p where
retrieveCredentials :: p -> IO CredentialValue
stsVersion :: ByteString
stsVersion = "2011-06-15"
defaultDurationSeconds :: Second
defaultDurationSeconds = 3600
data STSAssumeRole = STSAssumeRole
{ sarEndpoint :: Text,
sarCredentials :: CredentialValue,
sarOptions :: STSAssumeRoleOptions
}
data STSAssumeRoleOptions = STSAssumeRoleOptions
{ -- | Desired validity for the generated credentials.
saroDurationSeconds :: Maybe Second,
-- | IAM policy to apply for the generated credentials.
saroPolicyJSON :: Maybe ByteString,
-- | Location is usually required for AWS.
saroLocation :: Maybe Text,
saroRoleARN :: Maybe Text,
saroRoleSessionName :: Maybe Text,
-- | Optional HTTP connection manager
saroHTTPManager :: Maybe NC.Manager
}
-- | Default STS Assume Role options
defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
defaultSTSAssumeRoleOptions =
STSAssumeRoleOptions
{ saroDurationSeconds = Just defaultDurationSeconds,
saroPolicyJSON = Nothing,
saroLocation = Nothing,
saroRoleARN = Nothing,
saroRoleSessionName = Nothing,
saroHTTPManager = Nothing
}
instance CredentialProvider STSAssumeRole where
retrieveCredentials sar = do
-- Assemble STS request
let requiredParams =
[ ("Action", "AssumeRole"),
("Version", stsVersion)
]
opts = sarOptions sar
durSecs :: Int =
fromIntegral $
fromMaybe defaultDurationSeconds $
saroDurationSeconds opts
otherParams =
[ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
Just ("DurationSeconds", show durSecs),
("Policy",) <$> saroPolicyJSON opts
]
parameters = requiredParams ++ catMaybes otherParams
(host, port, isSecure) =
let endPt = NC.parseRequest_ $ toString $ sarEndpoint sar
in (NC.host endPt, NC.port endPt, NC.secure endPt)
reqBody = renderSimpleQuery False parameters
req =
NC.defaultRequest
{ NC.host = host,
NC.port = port,
NC.secure = isSecure,
NC.method = methodPost,
NC.requestHeaders =
[ (hHost, getHostHeader (host, port)),
(hContentType, "application/x-www-form-urlencoded")
],
NC.requestBody = RequestBodyBS reqBody
}
-- Sign the STS request.
timeStamp <- liftIO Time.getCurrentTime
let sp =
SignParams
{ spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
spService = ServiceSTS,
spTimeStamp = timeStamp,
spRegion = saroLocation opts,
spExpirySecs = Nothing,
spPayloadHash = Just $ hashSHA256 reqBody
}
signHeaders = signV4 sp req
signedReq =
req
{ NC.requestHeaders = NC.requestHeaders req ++ signHeaders
}
settings = bool NC.defaultManagerSettings NC.tlsManagerSettings isSecure
-- Make the STS request
mgr <- maybe (NC.newManager settings) return $ saroHTTPManager opts
resp <- httpLbs signedReq mgr
result <-
parseSTSAssumeRoleResult
(toStrict $ NC.responseBody resp)
"https://sts.amazonaws.com/doc/2011-06-15/"
return $ arcCredentials $ arrRoleCredentials result

View File

@ -232,16 +232,14 @@ isConnectInfoSecure = connectIsSecure
disableTLSCertValidation :: ConnectInfo -> ConnectInfo disableTLSCertValidation :: ConnectInfo -> ConnectInfo
disableTLSCertValidation c = c {connectDisableTLSCertValidation = True} disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
getHostAddr :: ConnectInfo -> ByteString getHostHeader :: (ByteString, Int) -> ByteString
getHostAddr ci = getHostHeader (host, port) =
if port == 80 || port == 443 if port == 80 || port == 443
then encodeUtf8 host then host
else else host <> ":" <> show port
encodeUtf8 $
T.concat [host, ":", show port] getHostAddr :: ConnectInfo -> ByteString
where getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
port = connectPort ci
host = connectHost ci
-- | Default Google Compute Storage ConnectInfo. Works only for -- | Default Google Compute Storage ConnectInfo. Works only for
-- "Simple Migration" use-case with interoperability mode enabled on -- "Simple Migration" use-case with interoperability mode enabled on
@ -1002,6 +1000,47 @@ type Stats = Progress
-- Select API Related Types End -- Select API Related Types End
-------------------------------------------------------------------------- --------------------------------------------------------------------------
----------------------------------------
-- Credentials Start
----------------------------------------
newtype AccessKey = AccessKey {unAccessKey :: Text}
deriving stock (Show)
deriving newtype (Eq, IsString)
newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
deriving stock (Show)
deriving newtype (Eq, IsString)
newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
deriving stock (Show)
deriving newtype (Eq, IsString)
data CredentialValue = CredentialValue
{ cvAccessKey :: AccessKey,
cvSecretKey :: SecretKey,
cvSessionToken :: Maybe SessionToken
}
deriving stock (Eq, Show)
data AssumeRoleCredentials = AssumeRoleCredentials
{ arcCredentials :: CredentialValue,
arcExpiration :: UTCTime
}
deriving stock (Show, Eq)
data AssumeRoleResult = AssumeRoleResult
{ arrSourceIdentity :: Text,
arrAssumedRoleArn :: Text,
arrAssumedRoleId :: Text,
arrRoleCredentials :: AssumeRoleCredentials
}
deriving stock (Show, Eq)
----------------------------------------
-- Credentials End
----------------------------------------
-- | Represents different kinds of payload that are used with S3 API -- | Represents different kinds of payload that are used with S3 API
-- requests. -- requests.
data Payload data Payload

View File

@ -39,6 +39,7 @@ where
import Data.Aeson ((.=)) import Data.Aeson ((.=))
import qualified Data.Aeson as Json import qualified Data.Aeson as Json
import qualified Data.ByteArray as BA
import Data.ByteString.Builder (byteString, toLazyByteString) import Data.ByteString.Builder (byteString, toLazyByteString)
import qualified Data.HashMap.Strict as H import qualified Data.HashMap.Strict as H
import qualified Data.Text as T import qualified Data.Text as T
@ -300,7 +301,7 @@ presignedPostPolicy p = do
ci <- asks mcConnInfo ci <- asks mcConnInfo
signTime <- liftIO Time.getCurrentTime signTime <- liftIO Time.getCurrentTime
let extraConditions = let extraConditions signParams =
[ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime), [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256", PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
PPCEquals PPCEquals
@ -308,23 +309,24 @@ presignedPostPolicy p = do
( T.intercalate ( T.intercalate
"/" "/"
[ connectAccessKey ci, [ connectAccessKey ci,
decodeUtf8 $ mkScope signTime region decodeUtf8 $ credentialScope signParams
] ]
) )
] ]
ppWithCreds = ppWithCreds signParams =
p p
{ conditions = conditions p ++ extraConditions { conditions = conditions p ++ extraConditions signParams
} }
sp = sp =
SignParams SignParams
(connectAccessKey ci) (connectAccessKey ci)
(connectSecretKey ci) (BA.convert (encodeUtf8 $ connectSecretKey ci :: ByteString))
ServiceS3
signTime signTime
(Just $ connectRegion ci) (Just $ connectRegion ci)
Nothing Nothing
Nothing Nothing
signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
-- compute form-data -- compute form-data
mkPair (PPCStartsWith k v) = Just (k, v) mkPair (PPCStartsWith k v) = Just (k, v)
mkPair (PPCEquals k v) = Just (k, v) mkPair (PPCEquals k v) = Just (k, v)
@ -334,12 +336,11 @@ presignedPostPolicy p = do
H.fromList $ H.fromList $
mapMaybe mapMaybe
mkPair mkPair
(conditions ppWithCreds) (conditions $ ppWithCreds sp)
formData = formFromPolicy `H.union` signData formData = formFromPolicy `H.union` signData
-- compute POST upload URL -- compute POST upload URL
bucket = H.lookupDefault "" "bucket" formData bucket = H.lookupDefault "" "bucket" formData
scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
region = connectRegion ci
url = url =
toStrictBS $ toStrictBS $
toLazyByteString $ toLazyByteString $

View File

@ -18,19 +18,22 @@
module Network.Minio.Sign.V4 where module Network.Minio.Sign.V4 where
import qualified Conduit as C import qualified Conduit as C
import qualified Data.ByteArray as BA
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy as LB
import Data.CaseInsensitive (mk)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set import qualified Data.HashSet as Set
import Data.List (partition)
import qualified Data.List.NonEmpty as NE
import qualified Data.Time as Time import qualified Data.Time as Time
import Lib.Prelude import Lib.Prelude
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (Header, parseQuery) import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types as H
import Network.HTTP.Types.Header (RequestHeaders)
import Network.Minio.Data.ByteString import Network.Minio.Data.ByteString
import Network.Minio.Data.Crypto import Network.Minio.Data.Crypto
import Network.Minio.Data.Time import Network.Minio.Data.Time
@ -60,9 +63,17 @@ data SignV4Data = SignV4Data
} }
deriving stock (Show) deriving stock (Show)
data Service = ServiceS3 | ServiceSTS
deriving stock (Eq, Show)
toByteString :: Service -> ByteString
toByteString ServiceS3 = "s3"
toByteString ServiceSTS = "sts"
data SignParams = SignParams data SignParams = SignParams
{ spAccessKey :: Text, { spAccessKey :: Text,
spSecretKey :: Text, spSecretKey :: BA.ScrubbedBytes,
spService :: Service,
spTimeStamp :: UTCTime, spTimeStamp :: UTCTime,
spRegion :: Maybe Text, spRegion :: Maybe Text,
spExpirySecs :: Maybe UrlExpiry, spExpirySecs :: Maybe UrlExpiry,
@ -102,6 +113,9 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
] ]
in (H.hAuthorization, authValue) in (H.hAuthorization, authValue)
data IsStreaming = IsStreamingLength Int64 | NotStreaming
deriving stock (Eq, Show)
-- | Given SignParams and request details, including request method, -- | Given SignParams and request details, including request method,
-- request path, headers, query params and payload hash, generates an -- request path, headers, query params and payload hash, generates an
-- updated set of headers, including the x-amz-date header and the -- updated set of headers, including the x-amz-date header and the
@ -114,33 +128,19 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
-- is being created. The expiry is interpreted as an integer number of -- is being created. The expiry is interpreted as an integer number of
-- seconds. The output will be the list of query-parameters to add to -- seconds. The output will be the list of query-parameters to add to
-- the request. -- the request.
signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)] signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
signV4 !sp !req = signV4QueryParams !sp !req =
let region = fromMaybe "" $ spRegion sp let scope = credentialScope sp
ts = spTimeStamp sp
scope = mkScope ts region
accessKey = encodeUtf8 $ spAccessKey sp
secretKey = encodeUtf8 $ spSecretKey sp
expiry = spExpirySecs sp expiry = spExpirySecs sp
sha256Hdr =
( "x-amz-content-sha256", headersToSign = getHeadersToSign $ NC.requestHeaders req
fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
)
-- headers to be added to the request
datePair = ("X-Amz-Date", awsTimeFormatBS ts)
computedHeaders =
NC.requestHeaders req
++ if isJust expiry
then []
else map (first mk) [datePair, sha256Hdr]
headersToSign = getHeadersToSign computedHeaders
signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
-- query-parameters to be added before signing for presigned URLs -- query-parameters to be added before signing for presigned URLs
-- (i.e. when `isJust expiry`) -- (i.e. when `isJust expiry`)
authQP = authQP =
[ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"), [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
("X-Amz-Credential", B.concat [accessKey, "/", scope]), ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
datePair, ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
("X-Amz-Expires", maybe "" showBS expiry), ("X-Amz-Expires", maybe "" showBS expiry),
("X-Amz-SignedHeaders", signedHeaderKeys) ("X-Amz-SignedHeaders", signedHeaderKeys)
] ]
@ -156,40 +156,129 @@ signV4 !sp !req =
sp sp
(NC.setQueryString finalQP req) (NC.setQueryString finalQP req)
headersToSign headersToSign
-- 2. compute string to sign -- 2. compute string to sign
stringToSign = mkStringToSign ts scope canonicalRequest stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
-- 3.1 compute signing key -- 3.1 compute signing key
signingKey = mkSigningKey ts region secretKey signingKey = getSigningKey sp
-- 3.2 compute signature
signature = computeSignature stringToSign signingKey
in ("X-Amz-Signature", signature) : authQP
-- | Given SignParams and request details, including request method, request
-- path, headers, query params and payload hash, generates an updated set of
-- headers, including the x-amz-date header and the Authorization header, which
-- includes the signature.
--
-- The output is the list of headers to be added to authenticate the request.
signV4 :: SignParams -> NC.Request -> [Header]
signV4 !sp !req =
let scope = credentialScope sp
-- extra headers to be added for signing purposes.
extraHeaders =
("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
: ( -- payload hash is only used for S3 (not STS)
[ ( "x-amz-content-sha256",
fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
)
| spService sp == ServiceS3
]
)
-- 1. compute canonical request
reqHeaders = NC.requestHeaders req ++ extraHeaders
(canonicalRequest, signedHeaderKeys) =
getCanonicalRequestAndSignedHeaders
NotStreaming
sp
req
reqHeaders
-- 2. compute string to sign
stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
-- 3.1 compute signing key
signingKey = getSigningKey sp
-- 3.2 compute signature -- 3.2 compute signature
signature = computeSignature stringToSign signingKey signature = computeSignature stringToSign signingKey
-- 4. compute auth header -- 4. compute auth header
authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
-- finally compute output pairs in authHeader : extraHeaders
output =
if isJust expiry
then ("X-Amz-Signature", signature) : authQP
else
[ first CI.foldedCase authHeader,
datePair,
sha256Hdr
]
in output
mkScope :: UTCTime -> Text -> ByteString credentialScope :: SignParams -> ByteString
mkScope ts region = credentialScope sp =
B.intercalate let region = fromMaybe "" $ spRegion sp
"/" in B.intercalate
[ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts, "/"
encodeUtf8 region, [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
"s3", encodeUtf8 region,
"aws4_request" toByteString $ spService sp,
] "aws4_request"
]
-- Folds header name, trims whitespace in header values, skips ignored headers
-- and sorts headers.
getHeadersToSign :: [Header] -> [(ByteString, ByteString)] getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
getHeadersToSign !h = getHeadersToSign !h =
filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $ filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
map (bimap CI.foldedCase stripBS) h map (bimap CI.foldedCase stripBS) h
-- | Given the list of headers in the request, computes the canonical headers
-- and the signed headers strings.
getCanonicalHeaders :: NonEmpty Header -> (ByteString, ByteString)
getCanonicalHeaders h =
let -- Folds header name, trims spaces in header values, skips ignored
-- headers and sorts headers by name (we must not re-order multi-valued
-- headers).
headersToSign =
NE.toList $
NE.sortBy (\a b -> compare (fst a) (fst b)) $
NE.fromList $
NE.filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
NE.map (bimap CI.foldedCase stripBS) h
canonicalHeaders = mconcat $ map (\(a, b) -> a <> ":" <> b <> "\n") headersToSign
signedHeaderKeys = B.intercalate ";" $ map fst headersToSign
in (canonicalHeaders, signedHeaderKeys)
getCanonicalRequestAndSignedHeaders ::
IsStreaming ->
SignParams ->
NC.Request ->
[Header] ->
(ByteString, ByteString)
getCanonicalRequestAndSignedHeaders isStreaming sp req requestHeaders =
let httpMethod = NC.method req
canonicalUri = uriEncode False $ NC.path req
canonicalQueryString =
B.intercalate "&" $
map (\(x, y) -> B.concat [x, "=", y]) $
sort $
map
( bimap (uriEncode True) (maybe "" (uriEncode True))
)
(parseQuery $ NC.queryString req)
(canonicalHeaders, signedHeaderKeys) = getCanonicalHeaders $ NE.fromList requestHeaders
payloadHashStr =
case isStreaming of
IsStreamingLength _ -> "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
NotStreaming -> fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
canonicalRequest =
B.intercalate
"\n"
[ httpMethod,
canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaderKeys,
payloadHashStr
]
in (canonicalRequest, signedHeaderKeys)
mkCanonicalRequest :: mkCanonicalRequest ::
Bool -> Bool ->
SignParams -> SignParams ->
@ -197,10 +286,12 @@ mkCanonicalRequest ::
[(ByteString, ByteString)] -> [(ByteString, ByteString)] ->
ByteString ByteString
mkCanonicalRequest !isStreaming !sp !req !headersForSign = mkCanonicalRequest !isStreaming !sp !req !headersForSign =
let canonicalQueryString = let httpMethod = NC.method req
canonicalUri = uriEncode False $ NC.path req
canonicalQueryString =
B.intercalate "&" $ B.intercalate "&" $
map (\(x, y) -> B.concat [x, "=", y]) $ map (\(x, y) -> B.concat [x, "=", y]) $
sort $ sortBy (\a b -> compare (fst a) (fst b)) $
map map
( bimap (uriEncode True) (maybe "" (uriEncode True)) ( bimap (uriEncode True) (maybe "" (uriEncode True))
) )
@ -216,8 +307,8 @@ mkCanonicalRequest !isStreaming !sp !req !headersForSign =
else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
in B.intercalate in B.intercalate
"\n" "\n"
[ NC.method req, [ httpMethod,
uriEncode False $ NC.path req, canonicalUri,
canonicalQueryString, canonicalQueryString,
canonicalHeaders, canonicalHeaders,
signedHeaders, signedHeaders,
@ -234,13 +325,13 @@ mkStringToSign ts !scope !canonicalRequest =
hashSHA256 canonicalRequest hashSHA256 canonicalRequest
] ]
mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString getSigningKey :: SignParams -> ByteString
mkSigningKey ts region !secretKey = getSigningKey sp =
hmacSHA256RawBS "aws4_request" hmacSHA256RawBS "aws4_request"
. hmacSHA256RawBS "s3" . hmacSHA256RawBS (toByteString $ spService sp)
. hmacSHA256RawBS (encodeUtf8 region) . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
. hmacSHA256RawBS (awsDateFormatBS ts) . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
$ B.concat ["AWS4", secretKey] $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
computeSignature :: ByteString -> ByteString -> ByteString computeSignature :: ByteString -> ByteString -> ByteString
computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@ -254,8 +345,7 @@ signV4PostPolicy ::
Map.HashMap Text ByteString Map.HashMap Text ByteString
signV4PostPolicy !postPolicyJSON !sp = signV4PostPolicy !postPolicyJSON !sp =
let stringToSign = Base64.encode postPolicyJSON let stringToSign = Base64.encode postPolicyJSON
region = fromMaybe "" $ spRegion sp signingKey = getSigningKey sp
signingKey = mkSigningKey (spTimeStamp sp) region $ encodeUtf8 $ spSecretKey sp
signature = computeSignature stringToSign signingKey signature = computeSignature stringToSign signingKey
in Map.fromList in Map.fromList
[ ("x-amz-signature", signature), [ ("x-amz-signature", signature),
@ -284,60 +374,59 @@ signedStreamLength dataLen =
finalChunkSize = 1 + 17 + 64 + 2 + 2 finalChunkSize = 1 + 17 + 64 + 2 + 2
in numChunks * fullChunkSize + lastChunkSize + finalChunkSize in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
-- For streaming S3, we need to update the content-encoding header.
addContentEncoding :: [Header] -> [Header]
addContentEncoding hs =
-- assume there is at most one content-encoding header.
let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
in maybe
(hContentEncoding, "aws-chunked")
(\(k, v) -> (k, v <> ",aws-chunked"))
(listToMaybe ceHdrs)
: others
signV4Stream :: signV4Stream ::
Int64 -> Int64 ->
SignParams -> SignParams ->
NC.Request -> NC.Request ->
(C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request) (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
-- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
signV4Stream !payloadLength !sp !req = signV4Stream !payloadLength !sp !req =
let ts = spTimeStamp sp let ts = spTimeStamp sp
addContentEncoding hs =
let ceMay = find (\(x, _) -> x == "content-encoding") hs -- compute the updated list of headers to be added for signing purposes.
in case ceMay of
Nothing -> ("content-encoding", "aws-chunked") : hs
Just (_, ce) ->
("content-encoding", ce <> ",aws-chunked")
: filter (\(x, _) -> x /= "content-encoding") hs
-- headers to be added to the request
datePair = ("X-Amz-Date", awsTimeFormatBS ts)
computedHeaders =
addContentEncoding $
datePair : NC.requestHeaders req
-- headers specific to streaming signature
signedContentLength = signedStreamLength payloadLength signedContentLength = signedStreamLength payloadLength
streamingHeaders :: [Header] extraHeaders =
streamingHeaders = [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
[ ("x-amz-decoded-content-length", showBS payloadLength), ("x-amz-decoded-content-length", showBS payloadLength),
("content-length", showBS signedContentLength), ("content-length", showBS signedContentLength),
("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD") ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
] ]
headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders requestHeaders =
signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign addContentEncoding $
finalQP = parseQuery (NC.queryString req) foldr setHeader (NC.requestHeaders req) extraHeaders
-- 1. Compute Seed Signature -- 1. Compute Seed Signature
-- 1.1 Canonical Request -- 1.1 Canonical Request
canonicalReq = (canonicalReq, signedHeaderKeys) =
mkCanonicalRequest getCanonicalRequestAndSignedHeaders
True (IsStreamingLength payloadLength)
sp sp
(NC.setQueryString finalQP req) req
headersToSign requestHeaders
region = fromMaybe "" $ spRegion sp
scope = mkScope ts region scope = credentialScope sp
accessKey = spAccessKey sp accessKey = spAccessKey sp
secretKey = spSecretKey sp
-- 1.2 String toSign -- 1.2 String toSign
stringToSign = mkStringToSign ts scope canonicalReq stringToSign = mkStringToSign ts scope canonicalReq
-- 1.3 Compute signature -- 1.3 Compute signature
-- 1.3.1 compute signing key -- 1.3.1 compute signing key
signingKey = mkSigningKey ts region $ encodeUtf8 secretKey signingKey = getSigningKey sp
-- 1.3.2 Compute signature -- 1.3.2 Compute signature
seedSignature = computeSignature stringToSign signingKey seedSignature = computeSignature stringToSign signingKey
-- 1.3.3 Compute Auth Header -- 1.3.3 Compute Auth Header
authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
-- 1.4 Updated headers for the request -- 1.4 Updated headers for the request
finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders) finalReqHeaders = authHeader : requestHeaders
-- headersToAdd = authHeader : datePair : streamingHeaders -- headersToAdd = authHeader : datePair : streamingHeaders
toHexStr n = B8.pack $ printf "%x" n toHexStr n = B8.pack $ printf "%x" n
@ -407,3 +496,9 @@ signV4Stream !payloadLength !sp !req =
NC.requestBodySource signedContentLength $ NC.requestBodySource signedContentLength $
src C..| signerConduit numParts lastPSize seedSignature src C..| signerConduit numParts lastPSize seedSignature
} }
-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
setHeader :: Header -> RequestHeaders -> RequestHeaders
setHeader hdr r =
let r' = filter (\(name, _) -> name /= fst hdr) r
in hdr : r'

View File

@ -27,9 +27,11 @@ module Network.Minio.XmlParser
parseErrResponse, parseErrResponse,
parseNotification, parseNotification,
parseSelectProgress, parseSelectProgress,
parseSTSAssumeRoleResult,
) )
where where
import qualified Data.ByteArray as BA
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy as LB
import qualified Data.HashMap.Strict as H import qualified Data.HashMap.Strict as H
import Data.List (zip4, zip6) import Data.List (zip4, zip6)
@ -220,8 +222,8 @@ parseListPartsResponse xmldata = do
parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
parseErrResponse xmldata = do parseErrResponse xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
let code = T.concat $ r $/ element "Code" &/ content let code = T.concat $ r $/ laxElement "Code" &/ content
message = T.concat $ r $/ element "Message" &/ content message = T.concat $ r $/ laxElement "Message" &/ content
return $ toServiceErr code message return $ toServiceErr code message
parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
@ -269,3 +271,102 @@ parseSelectProgress xmldata = do
<$> parseDecimal bScanned <$> parseDecimal bScanned
<*> parseDecimal bProcessed <*> parseDecimal bProcessed
<*> parseDecimal bReturned <*> parseDecimal bReturned
-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
-- <AssumeRoleResult>
-- <SourceIdentity>Alice</SourceIdentity>
-- <AssumedRoleUser>
-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
-- </AssumedRoleUser>
-- <Credentials>
-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
-- <SessionToken>
-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
-- </SessionToken>
-- <Expiration>2019-11-09T13:34:41Z</Expiration>
-- </Credentials>
-- <PackedPolicySize>6</PackedPolicySize>
-- </AssumeRoleResult>
-- <ResponseMetadata>
-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
-- </ResponseMetadata>
-- </AssumeRoleResponse>
parseSTSAssumeRoleResult :: MonadIO m => ByteString -> Text -> m AssumeRoleResult
parseSTSAssumeRoleResult xmldata namespace = do
r <- parseRoot $ LB.fromStrict xmldata
let s3Elem' = s3Elem namespace
sourceIdentity =
T.concat $
r
$/ s3Elem' "AssumeRoleResult"
&/ s3Elem' "SourceIdentity"
&/ content
roleArn =
T.concat $
r
$/ s3Elem' "AssumeRoleResult"
&/ s3Elem' "AssumedRoleUser"
&/ s3Elem' "Arn"
&/ content
roleId =
T.concat $
r
$/ s3Elem' "AssumeRoleResult"
&/ s3Elem' "AssumedRoleUser"
&/ s3Elem' "AssumedRoleId"
&/ content
convSB :: Text -> BA.ScrubbedBytes
convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
credsInfo = do
cr <-
maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
listToMaybe $
r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
let cur = fromNode $ node cr
return
( CredentialValue
{ cvAccessKey =
coerce $
T.concat $
cur $/ s3Elem' "AccessKeyId" &/ content,
cvSecretKey =
coerce $
convSB $
T.concat $
cur
$/ s3Elem' "SecretAccessKey"
&/ content,
cvSessionToken =
Just $
coerce $
convSB $
T.concat $
cur
$/ s3Elem' "SessionToken"
&/ content
},
T.concat $ cur $/ s3Elem' "Expiration" &/ content
)
creds <- either throwIO pure credsInfo
expiry <- parseS3XMLTime $ snd creds
let roleCredentials =
AssumeRoleCredentials
{ arcCredentials = fst creds,
arcExpiration = expiry
}
return
AssumeRoleResult
{ arrSourceIdentity = sourceIdentity,
arrAssumedRoleArn = roleArn,
arrAssumedRoleId = roleId,
arrRoleCredentials = roleCredentials
}

View File

@ -279,7 +279,8 @@ lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
fGetObject bucket object destFile defaultGetObjectOptions fGetObject bucket object destFile defaultGetObjectOptions
gotSize <- withNewHandle destFile getFileSize gotSize <- withNewHandle destFile getFileSize
liftIO $ liftIO $
gotSize == Right (Just mb15) gotSize
== Right (Just mb15)
@? "Wrong file size of put file after getting" @? "Wrong file size of put file after getting"
step "Cleanup actions" step "Cleanup actions"
@ -303,7 +304,8 @@ putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
fGetObject bucket obj destFile defaultGetObjectOptions fGetObject bucket obj destFile defaultGetObjectOptions
gotSize <- withNewHandle destFile getFileSize gotSize <- withNewHandle destFile getFileSize
liftIO $ liftIO $
gotSize == Right (Just mb1) gotSize
== Right (Just mb1)
@? "Wrong file size of put file after getting" @? "Wrong file size of put file after getting"
step "Cleanup actions" step "Cleanup actions"
@ -327,7 +329,8 @@ putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no siz
fGetObject bucket obj destFile defaultGetObjectOptions fGetObject bucket obj destFile defaultGetObjectOptions
gotSize <- withNewHandle destFile getFileSize gotSize <- withNewHandle destFile getFileSize
liftIO $ liftIO $
gotSize == Right (Just mb70) gotSize
== Right (Just mb70)
@? "Wrong file size of put file after getting" @? "Wrong file size of put file after getting"
step "Cleanup actions" step "Cleanup actions"
@ -569,6 +572,7 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
[] []
[] []
print putUrl
let size1 = 1000 :: Int64 let size1 = 1000 :: Int64
inputFile <- mkRandFile size1 inputFile <- mkRandFile size1
@ -1176,7 +1180,8 @@ getNPutSSECTest =
gotSize <- withNewHandle dstFile getFileSize gotSize <- withNewHandle dstFile getFileSize
liftIO $ liftIO $
gotSize == Right (Just mb1) gotSize
== Right (Just mb1)
@? "Wrong file size of object when getting" @? "Wrong file size of object when getting"
step "Cleanup" step "Cleanup"