git.iko.soy/typeable/minio-hs
on master help sign in
Add support for AssumeRole STS API (#188)

This change adds support for requesting temporary object storage credentials using the STS API. Some breaking changes are introduced to enable this support: - `Credentials` type has been removed. Use the `CredentialValue` type instead. Corresponding to this the type signature for `setCreds` has changed, though the functionality is the same. - The type alias `Provider` has been renamed to `CredentialLoader` to avoid naming confusion.

commitfa62ed599a108f005e2c4e40b0859838c346aa24 authorAditya Manthramurthy <donatello@users.noreply.github.com> dateWed 3 May 2023, 17:52 (3 years ago) parent7ae8a81
21 files changed+781−490
CHANGELOG.md+12−2 README.md+1−1 examples/AssumeRole.hs+26−12 minio-hs.cabal+6−3 src/Network/Minio.hs+21−7 src/Network/Minio/API.hs+24−4 src/Network/Minio/AdminAPI.hs+8−4 src/Network/Minio/Credentials.hs+47−114 src/Network/Minio/Credentials/AssumeRole.hs+264 src/Network/Minio/Credentials/Types.hs+85 src/Network/Minio/Data.hs+83−103 src/Network/Minio/Errors.hs+2−1 src/Network/Minio/PresignedOperations.hs+9−7 src/Network/Minio/S3API.hs+9−1 src/Network/Minio/Sign/V4.hs+21−31 src/Network/Minio/Utils.hs+26−44 src/Network/Minio/XmlCommon.hs+65 src/Network/Minio/XmlGenerator.hs+2−1 src/Network/Minio/XmlParser.hs+2−144 test/LiveServer.hs+67−9 test/Spec.hs+1−2
CHANGELOG.md+12−2
@@ -3,8 +3,18 @@ Changelog
33
44## Version 1.7.0 -- Unreleased
55
6−* Fix data type `EventMessage` to not export partial fields
7−* Bump up min bound on time dep and fix deprecation warnings.
6+* Fix data type `EventMessage` to not export partial fields (#179)
7+* Bump up min bound on time dep and fix deprecation warnings (#181)
8+* Add `dev` flag to cabal for building with warnings as errors (#182)
9+* Fix AWS region map (#185)
10+* Fix XML generator tests (#187)
11+* Add support for STS Assume Role API (#188)
12+
13+### Breaking changes in 1.7.0
14+
15+* `Credentials` type has been removed. Use `CredentialValue` instead.
16+* `Provider` type has been replaced with `CredentialLoader`.
17+* `EventMessage` data type is updated.
818
919## Version 1.6.0
1020
README.md+1−1
@@ -1,4 +1,4 @@
1−# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
1+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
22
33The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
44
examples/AssumeRole.hs+26−12
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2022 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,19 +15,33 @@
1515--
1616{-# LANGUAGE OverloadedStrings #-}
1717
18−import Network.Minio.Credentials
18+import Control.Monad.IO.Class (liftIO)
19+import Network.Minio
1920import Prelude
2021
2122main :: IO ()
2223main = do
23− res <-
24− retrieveCredentials
25− $ STSAssumeRole
26− "https://play.min.io"
27− ( CredentialValue
28− "Q3AM3UQ867SPQQA43P2F"
29− "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30− Nothing
31− )
32− $ defaultSTSAssumeRoleOptions {saroLocation = Just "us-east-1"}
24+ -- Use play credentials for example.
25+ let assumeRole =
26+ STSAssumeRole
27+ ( CredentialValue
28+ "Q3AM3UQ867SPQQA43P2F"
29+ "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30+ Nothing
31+ )
32+ $ defaultSTSAssumeRoleOptions
33+ { saroLocation = Just "us-east-1",
34+ saroEndpoint = Just "https://play.min.io:9000"
35+ }
36+
37+ -- Retrieve temporary credentials and print them.
38+ cv <- requestSTSCredential assumeRole
39+ print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
40+ print $ "Expiry" ++ show (snd cv)
41+
42+ -- Configure 'ConnectInfo' to request temporary credentials on demand.
43+ ci <- setSTSCredential assumeRole "https://play.min.io"
44+ res <- runMinio ci $ do
45+ buckets <- listBuckets
46+ liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
3347 print res
minio-hs.cabal+6−3
@@ -77,8 +77,6 @@ common base-settings
7777 , RankNTypes
7878 , ScopedTypeVariables
7979 , TupleSections
80− , TypeFamilies
81−
8280
8381 other-modules: Lib.Prelude
8482 , Network.Minio.API
@@ -97,7 +95,11 @@ common base-settings
9795 , Network.Minio.Utils
9896 , Network.Minio.XmlGenerator
9997 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
10099 , Network.Minio.JsonParser
100+ , Network.Minio.Credentials.Types
101+ , Network.Minio.Credentials.AssumeRole
102+ , Network.Minio.Credentials
101103
102104 mixins: base hiding (Prelude)
103105 , relude (Relude as Prelude)
@@ -142,7 +144,6 @@ library
142144 exposed-modules: Network.Minio
143145 , Network.Minio.AdminAPI
144146 , Network.Minio.S3API
145− , Network.Minio.Credentials
146147
147148Flag live-test
148149 Description: Build the test suite that runs against a live MinIO server
@@ -164,6 +165,7 @@ test-suite minio-hs-live-server-test
164165 , Network.Minio.Utils.Test
165166 , Network.Minio.XmlGenerator.Test
166167 , Network.Minio.XmlParser.Test
168+ , Network.Minio.Credentials
167169 build-depends: minio-hs
168170 , raw-strings-qq
169171 , tasty
@@ -197,6 +199,7 @@ test-suite minio-hs-test
197199 , Network.Minio.Utils.Test
198200 , Network.Minio.XmlGenerator.Test
199201 , Network.Minio.XmlParser.Test
202+ , Network.Minio.Credentials
200203
201204Flag examples
202205 Description: Build the examples
src/Network/Minio.hs+21−7
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
1616
1717-- |
1818-- Module: Network.Minio
19−-- Copyright: (c) 2017-2019 MinIO Dev Team
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
2020-- License: Apache 2.0
2121-- Maintainer: MinIO Dev Team <dev@min.io>
2222--
@@ -24,13 +24,17 @@
2424-- storage servers like MinIO.
2525module Network.Minio
2626 ( -- * Credentials
27− Credentials (..),
27+ CredentialValue (..),
28+ credentialValueText,
29+ AccessKey (..),
30+ SecretKey (..),
31+ SessionToken (..),
2832
29− -- ** Credential providers
33+ -- ** Credential Loaders
3034
31− -- | Run actions that retrieve 'Credentials' from the environment or
35+ -- | Run actions that retrieve 'CredentialValue's from the environment or
3236 -- files or other custom sources.
33− Provider,
37+ CredentialLoader,
3438 fromAWSConfigFile,
3539 fromAWSEnv,
3640 fromMinioEnv,
@@ -54,6 +58,15 @@ module Network.Minio
5458 awsCI,
5559 gcsCI,
5660
61+ -- ** STS Credential types
62+ STSAssumeRole (..),
63+ STSAssumeRoleOptions (..),
64+ defaultSTSAssumeRoleOptions,
65+ requestSTSCredential,
66+ setSTSCredential,
67+ ExpiryTime (..),
68+ STSCredentialProvider,
69+
5770 -- * Minio Monad
5871
5972 ----------------
@@ -225,14 +238,15 @@ This module exports the high-level MinIO API for object storage.
225238import qualified Data.Conduit as C
226239import qualified Data.Conduit.Binary as CB
227240import qualified Data.Conduit.Combinators as CC
241+import Network.Minio.API
228242import Network.Minio.CopyObject
243+import Network.Minio.Credentials
229244import Network.Minio.Data
230245import Network.Minio.Errors
231246import Network.Minio.ListOps
232247import Network.Minio.PutObject
233248import Network.Minio.S3API
234249import Network.Minio.SelectAPI
235−import Network.Minio.Utils
236250
237251-- | Lists buckets.
238252listBuckets :: Minio [BucketInfo]
src/Network/Minio/API.hs+24−4
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ module Network.Minio.API
2626 checkBucketNameValidity,
2727 isValidObjectName,
2828 checkObjectNameValidity,
29+ requestSTSCredential,
2930 )
3031where
3132
@@ -34,7 +35,6 @@ import Control.Retry
3435 limitRetriesByCumulativeDelay,
3536 retrying,
3637 )
37−import qualified Data.ByteArray as BA
3838import qualified Data.ByteString as B
3939import qualified Data.Char as C
4040import qualified Data.Conduit as C
@@ -42,6 +42,7 @@ import qualified Data.HashMap.Strict as H
4242import qualified Data.Text as T
4343import qualified Data.Time.Clock as Time
4444import Lib.Prelude
45+import Network.HTTP.Client (defaultManagerSettings)
4546import qualified Network.HTTP.Client as NClient
4647import Network.HTTP.Conduit (Response)
4748import qualified Network.HTTP.Conduit as NC
@@ -49,6 +50,7 @@ import Network.HTTP.Types (simpleQueryToQuery)
4950import qualified Network.HTTP.Types as HT
5051import Network.HTTP.Types.Header (hHost)
5152import Network.Minio.APICommon
53+import Network.Minio.Credentials
5254import Network.Minio.Data
5355import Network.Minio.Errors
5456import Network.Minio.Sign.V4
@@ -145,6 +147,20 @@ getHostPathRegion ri = do
145147 else return pathStyle
146148 )
147149
150+-- | requestSTSCredential requests temporary credentials using the Security Token
151+-- Service API. The returned credential will include a populated 'SessionToken'
152+-- and an 'ExpiryTime'.
153+requestSTSCredential :: STSCredentialProvider p => p -> IO (CredentialValue, ExpiryTime)
154+requestSTSCredential p = do
155+ endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
156+ let endPt = NC.parseRequest_ $ toString endpoint
157+ settings
158+ | NC.secure endPt = NC.tlsManagerSettings
159+ | otherwise = defaultManagerSettings
160+
161+ mgr <- NC.newManager settings
162+ liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
163+
148164buildRequest :: S3ReqInfo -> Minio NC.Request
149165buildRequest ri = do
150166 maybe (return ()) checkBucketNameValidity $ riBucket ri
@@ -175,10 +191,14 @@ buildRequest ri = do
175191
176192 timeStamp <- liftIO Time.getCurrentTime
177193
194+ mgr <- asks mcConnManager
195+ cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
196+
178197 let sp =
179198 SignParams
180− (connectAccessKey ci')
181− (BA.convert (encodeUtf8 $ connectSecretKey ci' :: ByteString))
199+ (coerce $ cvAccessKey cv)
200+ (coerce $ cvSecretKey cv)
201+ (coerce $ cvSessionToken cv)
182202 ServiceS3
183203 timeStamp
184204 (riRegion ri')
src/Network/Minio/AdminAPI.hs+8−4
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -70,7 +70,6 @@ import Data.Aeson
7070 )
7171import qualified Data.Aeson as A
7272import Data.Aeson.Types (typeMismatch)
73−import qualified Data.ByteArray as BA
7473import qualified Data.ByteString as B
7574import qualified Data.ByteString.Lazy as LBS
7675import qualified Data.Text as T
@@ -81,6 +80,7 @@ import qualified Network.HTTP.Conduit as NC
8180import qualified Network.HTTP.Types as HT
8281import Network.HTTP.Types.Header (hHost)
8382import Network.Minio.APICommon
83+import Network.Minio.Credentials
8484import Network.Minio.Data
8585import Network.Minio.Errors
8686import Network.Minio.Sign.V4
@@ -666,6 +666,9 @@ buildAdminRequest areq = do
666666
667667 timeStamp <- liftIO getCurrentTime
668668
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
669672 let hostHeader = (hHost, getHostAddr ci)
670673 newAreq =
671674 areq
@@ -678,8 +681,9 @@ buildAdminRequest areq = do
678681 signReq = toRequest ci newAreq
679682 sp =
680683 SignParams
681− (connectAccessKey ci)
682− (BA.convert (encodeUtf8 $ connectSecretKey ci :: ByteString))
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
683687 ServiceS3
684688 timeStamp
685689 Nothing
src/Network/Minio/Credentials.hs+47−114
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2022 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,129 +16,62 @@
1616
1717module Network.Minio.Credentials
1818 ( CredentialValue (..),
19− CredentialProvider (..),
20− AccessKey,
21− SecretKey,
22− SessionToken,
19+ credentialValueText,
20+ STSCredentialProvider (..),
21+ AccessKey (..),
22+ SecretKey (..),
23+ SessionToken (..),
24+ ExpiryTime (..),
25+ STSCredentialStore,
26+ initSTSCredential,
27+ getSTSCredential,
28+ Creds (..),
29+ getCredential,
30+ Endpoint,
31+
32+ -- * STS Assume Role
2333 defaultSTSAssumeRoleOptions,
2434 STSAssumeRole (..),
2535 STSAssumeRoleOptions (..),
2636 )
2737where
2838
29−import qualified Data.Time as Time
30−import Data.Time.Units (Second)
31−import Network.HTTP.Client (RequestBody (RequestBodyBS))
39+import Data.Time (diffUTCTime, getCurrentTime)
3240import qualified Network.HTTP.Client as NC
33−import qualified Network.HTTP.Client.TLS as NC
34−import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
35−import Network.HTTP.Types.Header (hHost)
36−import Network.Minio.Data
37−import Network.Minio.Data.Crypto (hashSHA256)
38−import Network.Minio.Sign.V4
39−import Network.Minio.Utils (httpLbs)
40−import Network.Minio.XmlParser (parseSTSAssumeRoleResult)
41−
42−class CredentialProvider p where
43− retrieveCredentials :: p -> IO CredentialValue
44−
45−stsVersion :: ByteString
46−stsVersion = "2011-06-15"
47−
48−defaultDurationSeconds :: Second
49−defaultDurationSeconds = 3600
50−
51−data STSAssumeRole = STSAssumeRole
52− { sarEndpoint :: Text,
53− sarCredentials :: CredentialValue,
54− sarOptions :: STSAssumeRoleOptions
55− }
41+import Network.Minio.Credentials.AssumeRole
42+import Network.Minio.Credentials.Types
43+import qualified UnliftIO.MVar as M
5644
57−data STSAssumeRoleOptions = STSAssumeRoleOptions
58− { -- | Desired validity for the generated credentials.
59− saroDurationSeconds :: Maybe Second,
60− -- | IAM policy to apply for the generated credentials.
61− saroPolicyJSON :: Maybe ByteString,
62− -- | Location is usually required for AWS.
63− saroLocation :: Maybe Text,
64− saroRoleARN :: Maybe Text,
65− saroRoleSessionName :: Maybe Text,
66− -- | Optional HTTP connection manager
67− saroHTTPManager :: Maybe NC.Manager
45+data STSCredentialStore = STSCredentialStore
46+ { cachedCredentials :: M.MVar (CredentialValue, ExpiryTime),
47+ refreshAction :: Endpoint -> NC.Manager -> IO (CredentialValue, ExpiryTime)
6848 }
6949
70−-- | Default STS Assume Role options
71−defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
72−defaultSTSAssumeRoleOptions =
73− STSAssumeRoleOptions
74− { saroDurationSeconds = Just defaultDurationSeconds,
75− saroPolicyJSON = Nothing,
76− saroLocation = Nothing,
77− saroRoleARN = Nothing,
78− saroRoleSessionName = Nothing,
79− saroHTTPManager = Nothing
80− }
50+initSTSCredential :: STSCredentialProvider p => p -> IO STSCredentialStore
51+initSTSCredential p = do
52+ let action = retrieveSTSCredentials p
53+ -- start with dummy credential, so that refresh happens for first request.
54+ now <- getCurrentTime
55+ mvar <- M.newMVar (CredentialValue mempty mempty mempty, coerce now)
56+ return $
57+ STSCredentialStore
58+ { cachedCredentials = mvar,
59+ refreshAction = action
60+ }
8161
82−instance CredentialProvider STSAssumeRole where
83− retrieveCredentials sar = do
84− -- Assemble STS request
85− let requiredParams =
86− [ ("Action", "AssumeRole"),
87− ("Version", stsVersion)
88− ]
89− opts = sarOptions sar
90− durSecs :: Int =
91− fromIntegral $
92− fromMaybe defaultDurationSeconds $
93− saroDurationSeconds opts
94− otherParams =
95− [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
96− ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
97− Just ("DurationSeconds", show durSecs),
98− ("Policy",) <$> saroPolicyJSON opts
99− ]
100− parameters = requiredParams ++ catMaybes otherParams
101− (host, port, isSecure) =
102− let endPt = NC.parseRequest_ $ toString $ sarEndpoint sar
103− in (NC.host endPt, NC.port endPt, NC.secure endPt)
104− reqBody = renderSimpleQuery False parameters
105− req =
106− NC.defaultRequest
107− { NC.host = host,
108− NC.port = port,
109− NC.secure = isSecure,
110− NC.method = methodPost,
111− NC.requestHeaders =
112− [ (hHost, getHostHeader (host, port)),
113− (hContentType, "application/x-www-form-urlencoded")
114− ],
115− NC.requestBody = RequestBodyBS reqBody
116− }
62+getSTSCredential :: STSCredentialStore -> Endpoint -> NC.Manager -> IO (CredentialValue, Bool)
63+getSTSCredential store ep mgr = M.modifyMVar (cachedCredentials store) $ \cc@(v, expiry) -> do
64+ now <- getCurrentTime
65+ if diffUTCTime now (coerce expiry) > 0
66+ then do
67+ res <- refreshAction store ep mgr
68+ return (res, (fst res, True))
69+ else return (cc, (v, False))
11770
118− -- Sign the STS request.
119− timeStamp <- liftIO Time.getCurrentTime
120− let sp =
121− SignParams
122− { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
123− spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
124− spService = ServiceSTS,
125− spTimeStamp = timeStamp,
126− spRegion = saroLocation opts,
127− spExpirySecs = Nothing,
128− spPayloadHash = Just $ hashSHA256 reqBody
129− }
130− signHeaders = signV4 sp req
131− signedReq =
132− req
133− { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
134− }
135− settings = bool NC.defaultManagerSettings NC.tlsManagerSettings isSecure
71+data Creds
72+ = CredsStatic CredentialValue
73+ | CredsSTS STSCredentialStore
13674
137− -- Make the STS request
138− mgr <- maybe (NC.newManager settings) return $ saroHTTPManager opts
139− resp <- httpLbs signedReq mgr
140− result <-
141− parseSTSAssumeRoleResult
142− (toStrict $ NC.responseBody resp)
143− "https://sts.amazonaws.com/doc/2011-06-15/"
144− return $ arcCredentials $ arrRoleCredentials result
75+getCredential :: Creds -> Endpoint -> NC.Manager -> IO CredentialValue
76+getCredential (CredsStatic v) _ _ = return v
77+getCredential (CredsSTS s) ep mgr = fst <$> getSTSCredential s ep mgr
src/Network/Minio/Credentials/AssumeRole.hsnew file+264
@@ -0,0 +1,264 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials.AssumeRole where
18+
19+import qualified Data.ByteArray as BA
20+import qualified Data.ByteString.Lazy as LB
21+import qualified Data.Text as T
22+import qualified Data.Time as Time
23+import Data.Time.Units (Second)
24+import Lib.Prelude (UTCTime, throwIO)
25+import Network.HTTP.Client (RequestBody (RequestBodyBS))
26+import qualified Network.HTTP.Client as NC
27+import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
28+import Network.HTTP.Types.Header (hHost)
29+import Network.Minio.Credentials.Types
30+import Network.Minio.Data.Crypto (hashSHA256)
31+import Network.Minio.Errors (MErrV (..))
32+import Network.Minio.Sign.V4
33+import Network.Minio.Utils (getHostHeader, httpLbs)
34+import Network.Minio.XmlCommon
35+import Text.XML.Cursor hiding (bool)
36+
37+stsVersion :: ByteString
38+stsVersion = "2011-06-15"
39+
40+defaultDurationSeconds :: Second
41+defaultDurationSeconds = 3600
42+
43+-- | Assume Role API argument.
44+data STSAssumeRole = STSAssumeRole
45+ { -- | Credentials to use in the AssumeRole STS API.
46+ sarCredentials :: CredentialValue,
47+ -- | Optional settings.
48+ sarOptions :: STSAssumeRoleOptions
49+ }
50+
51+-- | Options for STS Assume Role API.
52+data STSAssumeRoleOptions = STSAssumeRoleOptions
53+ { -- | STS endpoint to which the request will be made. For MinIO, this is the
54+ -- same as the server endpoint. For AWS, this has to be the Security Token
55+ -- Service endpoint. If using with 'setSTSCredential', this option can be
56+ -- left as 'Nothing' and the endpoint in 'ConnectInfo' will be used.
57+ saroEndpoint :: Maybe Text,
58+ -- | Desired validity for the generated credentials.
59+ saroDurationSeconds :: Maybe Second,
60+ -- | IAM policy to apply for the generated credentials.
61+ saroPolicyJSON :: Maybe ByteString,
62+ -- | Location is usually required for AWS.
63+ saroLocation :: Maybe Text,
64+ saroRoleARN :: Maybe Text,
65+ saroRoleSessionName :: Maybe Text
66+ }
67+
68+-- | Default STS Assume Role options - all options are Nothing, except for
69+-- duration which is set to 1 hour.
70+defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
71+defaultSTSAssumeRoleOptions =
72+ STSAssumeRoleOptions
73+ { saroEndpoint = Nothing,
74+ saroDurationSeconds = Just 3600,
75+ saroPolicyJSON = Nothing,
76+ saroLocation = Nothing,
77+ saroRoleARN = Nothing,
78+ saroRoleSessionName = Nothing
79+ }
80+
81+data AssumeRoleCredentials = AssumeRoleCredentials
82+ { arcCredentials :: CredentialValue,
83+ arcExpiration :: UTCTime
84+ }
85+ deriving stock (Show, Eq)
86+
87+data AssumeRoleResult = AssumeRoleResult
88+ { arrSourceIdentity :: Text,
89+ arrAssumedRoleArn :: Text,
90+ arrAssumedRoleId :: Text,
91+ arrRoleCredentials :: AssumeRoleCredentials
92+ }
93+ deriving stock (Show, Eq)
94+
95+-- | parseSTSAssumeRoleResult parses an XML response of the following form:
96+--
97+-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
98+-- <AssumeRoleResult>
99+-- <SourceIdentity>Alice</SourceIdentity>
100+-- <AssumedRoleUser>
101+-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
102+-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
103+-- </AssumedRoleUser>
104+-- <Credentials>
105+-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
106+-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
107+-- <SessionToken>
108+-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
109+-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
110+-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
111+-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
112+-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
113+-- </SessionToken>
114+-- <Expiration>2019-11-09T13:34:41Z</Expiration>
115+-- </Credentials>
116+-- <PackedPolicySize>6</PackedPolicySize>
117+-- </AssumeRoleResult>
118+-- <ResponseMetadata>
119+-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
120+-- </ResponseMetadata>
121+-- </AssumeRoleResponse>
122+parseSTSAssumeRoleResult :: MonadIO m => ByteString -> Text -> m AssumeRoleResult
123+parseSTSAssumeRoleResult xmldata namespace = do
124+ r <- parseRoot $ LB.fromStrict xmldata
125+ let s3Elem' = s3Elem namespace
126+ sourceIdentity =
127+ T.concat $
128+ r
129+ $/ s3Elem' "AssumeRoleResult"
130+ &/ s3Elem' "SourceIdentity"
131+ &/ content
132+ roleArn =
133+ T.concat $
134+ r
135+ $/ s3Elem' "AssumeRoleResult"
136+ &/ s3Elem' "AssumedRoleUser"
137+ &/ s3Elem' "Arn"
138+ &/ content
139+ roleId =
140+ T.concat $
141+ r
142+ $/ s3Elem' "AssumeRoleResult"
143+ &/ s3Elem' "AssumedRoleUser"
144+ &/ s3Elem' "AssumedRoleId"
145+ &/ content
146+
147+ convSB :: Text -> BA.ScrubbedBytes
148+ convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
149+
150+ credsInfo = do
151+ cr <-
152+ maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
153+ listToMaybe $
154+ r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
155+ let cur = fromNode $ node cr
156+ return
157+ ( CredentialValue
158+ { cvAccessKey =
159+ coerce $
160+ T.concat $
161+ cur $/ s3Elem' "AccessKeyId" &/ content,
162+ cvSecretKey =
163+ coerce $
164+ convSB $
165+ T.concat $
166+ cur
167+ $/ s3Elem' "SecretAccessKey"
168+ &/ content,
169+ cvSessionToken =
170+ Just $
171+ coerce $
172+ convSB $
173+ T.concat $
174+ cur
175+ $/ s3Elem' "SessionToken"
176+ &/ content
177+ },
178+ T.concat $ cur $/ s3Elem' "Expiration" &/ content
179+ )
180+ creds <- either throwIO pure credsInfo
181+ expiry <- parseS3XMLTime $ snd creds
182+ let roleCredentials =
183+ AssumeRoleCredentials
184+ { arcCredentials = fst creds,
185+ arcExpiration = expiry
186+ }
187+ return
188+ AssumeRoleResult
189+ { arrSourceIdentity = sourceIdentity,
190+ arrAssumedRoleArn = roleArn,
191+ arrAssumedRoleId = roleId,
192+ arrRoleCredentials = roleCredentials
193+ }
194+
195+instance STSCredentialProvider STSAssumeRole where
196+ getSTSEndpoint = saroEndpoint . sarOptions
197+ retrieveSTSCredentials sar (host', port', isSecure') mgr = do
198+ -- Assemble STS request
199+ let requiredParams =
200+ [ ("Action", "AssumeRole"),
201+ ("Version", stsVersion)
202+ ]
203+ opts = sarOptions sar
204+
205+ durSecs :: Int =
206+ fromIntegral $
207+ fromMaybe defaultDurationSeconds $
208+ saroDurationSeconds opts
209+ otherParams =
210+ [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
211+ ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
212+ Just ("DurationSeconds", show durSecs),
213+ ("Policy",) <$> saroPolicyJSON opts
214+ ]
215+ parameters = requiredParams ++ catMaybes otherParams
216+ (host, port, isSecure) =
217+ case getSTSEndpoint sar of
218+ Just ep ->
219+ let endPt = NC.parseRequest_ $ toString ep
220+ in (NC.host endPt, NC.port endPt, NC.secure endPt)
221+ Nothing -> (host', port', isSecure')
222+ reqBody = renderSimpleQuery False parameters
223+ req =
224+ NC.defaultRequest
225+ { NC.host = host,
226+ NC.port = port,
227+ NC.secure = isSecure,
228+ NC.method = methodPost,
229+ NC.requestHeaders =
230+ [ (hHost, getHostHeader (host, port)),
231+ (hContentType, "application/x-www-form-urlencoded")
232+ ],
233+ NC.requestBody = RequestBodyBS reqBody
234+ }
235+
236+ -- Sign the STS request.
237+ timeStamp <- liftIO Time.getCurrentTime
238+ let sp =
239+ SignParams
240+ { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
241+ spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
242+ spSessionToken = coerce $ cvSessionToken $ sarCredentials sar,
243+ spService = ServiceSTS,
244+ spTimeStamp = timeStamp,
245+ spRegion = saroLocation opts,
246+ spExpirySecs = Nothing,
247+ spPayloadHash = Just $ hashSHA256 reqBody
248+ }
249+ signHeaders = signV4 sp req
250+ signedReq =
251+ req
252+ { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
253+ }
254+
255+ -- Make the STS request
256+ resp <- httpLbs signedReq mgr
257+ result <-
258+ parseSTSAssumeRoleResult
259+ (toStrict $ NC.responseBody resp)
260+ "https://sts.amazonaws.com/doc/2011-06-15/"
261+ return
262+ ( arcCredentials $ arrRoleCredentials result,
263+ coerce $ arcExpiration $ arrRoleCredentials result
264+ )
src/Network/Minio/Credentials/Types.hsnew file+85
@@ -0,0 +1,85 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+
18+module Network.Minio.Credentials.Types where
19+
20+import qualified Data.ByteArray as BA
21+import Lib.Prelude (UTCTime)
22+import qualified Network.HTTP.Client as NC
23+
24+-- | Access Key type.
25+newtype AccessKey = AccessKey {unAccessKey :: Text}
26+ deriving stock (Show)
27+ deriving newtype (Eq, IsString, Semigroup, Monoid)
28+
29+-- | Secret Key type - has a show instance that does not print the value.
30+newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
31+ deriving stock (Show)
32+ deriving newtype (Eq, IsString, Semigroup, Monoid)
33+
34+-- | Session Token type - has a show instance that does not print the value.
35+newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
36+ deriving stock (Show)
37+ deriving newtype (Eq, IsString, Semigroup, Monoid)
38+
39+-- | Object storage credential data type. It has support for the optional
40+-- <https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html
41+-- SessionToken> for using temporary credentials requested via STS.
42+--
43+-- The show instance for this type does not print the value of secrets for
44+-- security.
45+data CredentialValue = CredentialValue
46+ { cvAccessKey :: AccessKey,
47+ cvSecretKey :: SecretKey,
48+ cvSessionToken :: Maybe SessionToken
49+ }
50+ deriving stock (Eq, Show)
51+
52+scrubbedToText :: BA.ScrubbedBytes -> Text
53+scrubbedToText =
54+ let b2t :: ByteString -> Text
55+ b2t = decodeUtf8
56+ s2b :: BA.ScrubbedBytes -> ByteString
57+ s2b = BA.convert
58+ in b2t . s2b
59+
60+-- | Convert a 'CredentialValue' to a text tuple. Use this to output the
61+-- credential to files or other programs.
62+credentialValueText :: CredentialValue -> (Text, Text, Maybe Text)
63+credentialValueText cv =
64+ ( coerce $ cvAccessKey cv,
65+ (scrubbedToText . coerce) $ cvSecretKey cv,
66+ scrubbedToText . coerce <$> cvSessionToken cv
67+ )
68+
69+-- | Endpoint represented by host, port and TLS enabled flag.
70+type Endpoint = (ByteString, Int, Bool)
71+
72+-- | Typeclass for STS credential providers.
73+class STSCredentialProvider p where
74+ retrieveSTSCredentials ::
75+ p ->
76+ -- | STS Endpoint (host, port, isSecure)
77+ Endpoint ->
78+ NC.Manager ->
79+ IO (CredentialValue, ExpiryTime)
80+ getSTSEndpoint :: p -> Maybe Text
81+
82+-- | 'ExpiryTime' represents a time at which a credential expires.
83+newtype ExpiryTime = ExpiryTime {unExpiryTime :: UTCTime}
84+ deriving stock (Show)
85+ deriving newtype (Eq)
src/Network/Minio/Data.hs+83−103
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -34,9 +34,9 @@ import qualified Data.Aeson as A
3434import qualified Data.ByteArray as BA
3535import qualified Data.ByteString as B
3636import qualified Data.ByteString.Lazy as LB
37−import Data.CaseInsensitive (mk)
3837import qualified Data.HashMap.Strict as H
3938import qualified Data.Ini as Ini
39+import qualified Data.List as List
4040import qualified Data.Text as T
4141import qualified Data.Text.Encoding as TE
4242import Data.Time (defaultTimeLocale, formatTime)
@@ -53,6 +53,7 @@ import Network.HTTP.Types
5353 hRange,
5454 )
5555import qualified Network.HTTP.Types as HT
56+import Network.Minio.Credentials
5657import Network.Minio.Data.Crypto
5758 ( encodeToBase64,
5859 hashMD5ToBase64,
@@ -62,11 +63,12 @@ import Network.Minio.Errors
6263 ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
6364 MinioErr (..),
6465 )
66+import Network.Minio.Utils
6567import System.Directory (doesFileExist, getHomeDirectory)
6668import qualified System.Environment as Env
6769import System.FilePath.Posix (combine)
68−import Text.XML (Name (Name))
6970import qualified UnliftIO as U
71+import qualified UnliftIO.MVar as UM
7072
7173-- | max obj size is 5TiB
7274maxObjectSize :: Int64
@@ -131,14 +133,15 @@ awsRegionMap =
131133data ConnectInfo = ConnectInfo
132134 { connectHost :: Text,
133135 connectPort :: Int,
134− connectAccessKey :: Text,
135− connectSecretKey :: Text,
136+ connectCreds :: Creds,
136137 connectIsSecure :: Bool,
137138 connectRegion :: Region,
138139 connectAutoDiscoverRegion :: Bool,
139140 connectDisableTLSCertValidation :: Bool
140141 }
141− deriving stock (Eq, Show)
142+
143+getEndpoint :: ConnectInfo -> Endpoint
144+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
142145
143146instance IsString ConnectInfo where
144147 fromString str =
@@ -146,8 +149,7 @@ instance IsString ConnectInfo where
146149 in ConnectInfo
147150 { connectHost = TE.decodeUtf8 $ NC.host req,
148151 connectPort = NC.port req,
149− connectAccessKey = "",
150− connectSecretKey = "",
152+ connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
151153 connectIsSecure = NC.secure req,
152154 connectRegion = "",
153155 connectAutoDiscoverRegion = True,
@@ -161,20 +163,21 @@ data Credentials = Credentials
161163 }
162164 deriving stock (Eq, Show)
163165
164−-- | A Provider is an action that may return Credentials. Providers
165−-- may be chained together using 'findFirst'.
166−type Provider = IO (Maybe Credentials)
166+-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
167+-- Loaders may be chained together using 'findFirst'.
168+type CredentialLoader = IO (Maybe CredentialValue)
167169
168−-- | Combines the given list of providers, by calling each one in
169−-- order until Credentials are found.
170−findFirst :: [Provider] -> Provider
170+-- | Combines the given list of loaders, by calling each one in
171+-- order until a 'CredentialValue' is returned.
172+findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
171173findFirst [] = return Nothing
172174findFirst (f : fs) = do
173175 c <- f
174176 maybe (findFirst fs) (return . Just) c
175177
176−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
177−fromAWSConfigFile :: Provider
178+-- | This action returns a 'CredentialValue' populated from
179+-- @~\/.aws\/credentials@
180+fromAWSConfigFile :: CredentialLoader
178181fromAWSConfigFile = do
179182 credsE <- runExceptT $ do
180183 homeDir <- lift getHomeDirectory
@@ -190,29 +193,28 @@ fromAWSConfigFile = do
190193 ExceptT $
191194 return $
192195 Ini.lookupValue "default" "aws_secret_access_key" ini
193− return $ Credentials akey skey
196+ return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
194197 return $ either (const Nothing) Just credsE
195198
196−-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
197−-- @AWS_SECRET_ACCESS_KEY@ environment variables.
198−fromAWSEnv :: Provider
199+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
200+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
201+fromAWSEnv :: CredentialLoader
199202fromAWSEnv = runMaybeT $ do
200203 akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
201204 skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
202− return $ Credentials (T.pack akey) (T.pack skey)
205+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
203206
204−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
205−-- @MINIO_SECRET_KEY@ environment variables.
206−fromMinioEnv :: Provider
207+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
208+-- and @MINIO_SECRET_KEY@ environment variables.
209+fromMinioEnv :: CredentialLoader
207210fromMinioEnv = runMaybeT $ do
208211 akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
209212 skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
210− return $ Credentials (T.pack akey) (T.pack skey)
213+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
211214
212−-- | setCredsFrom retrieves access credentials from the first
213−-- `Provider` form the given list that succeeds and sets it in the
214−-- `ConnectInfo`.
215−setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
215+-- | setCredsFrom retrieves access credentials from the first action in the
216+-- given list that succeeds and sets it in the 'ConnectInfo'.
217+setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
216218setCredsFrom ps ci = do
217219 pMay <- findFirst ps
218220 maybe
@@ -220,14 +222,21 @@ setCredsFrom ps ci = do
220222 (return . (`setCreds` ci))
221223 pMay
222224
223−-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
224−setCreds :: Credentials -> ConnectInfo -> ConnectInfo
225−setCreds (Credentials accessKey secretKey) connInfo =
225+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
226+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
227+setCreds cv connInfo =
226228 connInfo
227− { connectAccessKey = accessKey,
228− connectSecretKey = secretKey
229+ { connectCreds = CredsStatic cv
229230 }
230231
232+-- | 'setSTSCredential' configures `ConnectInfo` to retrieve temporary
233+-- credentials via the STS API on demand. It is automatically refreshed on
234+-- expiry.
235+setSTSCredential :: STSCredentialProvider p => p -> ConnectInfo -> IO ConnectInfo
236+setSTSCredential p ci = do
237+ store <- initSTSCredential p
238+ return ci {connectCreds = CredsSTS store}
239+
231240-- | Set the S3 region parameter in the `ConnectInfo`
232241setRegion :: Region -> ConnectInfo -> ConnectInfo
233242setRegion r connInfo =
@@ -248,12 +257,6 @@ isConnectInfoSecure = connectIsSecure
248257disableTLSCertValidation :: ConnectInfo -> ConnectInfo
249258disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
250259
251−getHostHeader :: (ByteString, Int) -> ByteString
252−getHostHeader (host, port) =
253− if port == 80 || port == 443
254− then host
255− else host <> ":" <> show port
256−
257260getHostAddr :: ConnectInfo -> ByteString
258261getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
259262
@@ -278,7 +281,7 @@ awsCI = "https://s3.amazonaws.com"
278281-- ConnectInfo. Credentials are already filled in.
279282minioPlayCI :: ConnectInfo
280283minioPlayCI =
281− let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
284+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
282285 in setCreds playCreds $
283286 setRegion
284287 "us-east-1"
@@ -380,24 +383,6 @@ data PutObjectOptions = PutObjectOptions
380383defaultPutObjectOptions :: PutObjectOptions
381384defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
382385
383−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
384−-- stripped and a Just is returned.
385−userMetadataHeaderNameMaybe :: Text -> Maybe Text
386−userMetadataHeaderNameMaybe k =
387− let prefix = T.toCaseFold "X-Amz-Meta-"
388− n = T.length prefix
389− in if T.toCaseFold (T.take n k) == prefix
390− then Just (T.drop n k)
391− else Nothing
392−
393−addXAmzMetaPrefix :: Text -> Text
394−addXAmzMetaPrefix s
395− | isJust (userMetadataHeaderNameMaybe s) = s
396− | otherwise = "X-Amz-Meta-" <> s
397−
398−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
399−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
400−
401386pooToHeaders :: PutObjectOptions -> [HT.Header]
402387pooToHeaders poo =
403388 userMetadata
@@ -437,6 +422,29 @@ data BucketInfo = BucketInfo
437422-- | A type alias to represent a part-number for multipart upload
438423type PartNumber = Int16
439424
425+-- | Select part sizes - the logic is that the minimum part-size will
426+-- be 64MiB.
427+selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
428+selectPartSizes size =
429+ uncurry (List.zip3 [1 ..]) $
430+ List.unzip $
431+ loop 0 size
432+ where
433+ ceil :: Double -> Int64
434+ ceil = ceiling
435+ partSize =
436+ max
437+ minPartSize
438+ ( ceil $
439+ fromIntegral size
440+ / fromIntegral maxMultipartParts
441+ )
442+ m = partSize
443+ loop st sz
444+ | st > sz = []
445+ | st + m >= sz = [(st, sz - st)]
446+ | otherwise = (st, m) : loop (st + m) sz
447+
440448-- | A type alias to represent an upload-id for multipart upload
441449type UploadId = Text
442450
@@ -1016,47 +1024,6 @@ type Stats = Progress
10161024-- Select API Related Types End
10171025--------------------------------------------------------------------------
10181026
1019−----------------------------------------
1020−-- Credentials Start
1021−----------------------------------------
1022−
1023−newtype AccessKey = AccessKey {unAccessKey :: Text}
1024− deriving stock (Show)
1025− deriving newtype (Eq, IsString)
1026−
1027−newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
1028− deriving stock (Show)
1029− deriving newtype (Eq, IsString)
1030−
1031−newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
1032− deriving stock (Show)
1033− deriving newtype (Eq, IsString)
1034−
1035−data CredentialValue = CredentialValue
1036− { cvAccessKey :: AccessKey,
1037− cvSecretKey :: SecretKey,
1038− cvSessionToken :: Maybe SessionToken
1039− }
1040− deriving stock (Eq, Show)
1041−
1042−data AssumeRoleCredentials = AssumeRoleCredentials
1043− { arcCredentials :: CredentialValue,
1044− arcExpiration :: UTCTime
1045− }
1046− deriving stock (Show, Eq)
1047−
1048−data AssumeRoleResult = AssumeRoleResult
1049− { arrSourceIdentity :: Text,
1050− arrAssumedRoleArn :: Text,
1051− arrAssumedRoleId :: Text,
1052− arrRoleCredentials :: AssumeRoleCredentials
1053− }
1054− deriving stock (Show, Eq)
1055−
1056−----------------------------------------
1057−-- Credentials End
1058−----------------------------------------
1059−
10601027-- | Represents different kinds of payload that are used with S3 API
10611028-- requests.
10621029data Payload
@@ -1202,9 +1169,22 @@ runMinioRes ci m = do
12021169 conn <- liftIO $ connect ci
12031170 runMinioResWith conn m
12041171
1205−s3Name :: Text -> Text -> Name
1206−s3Name ns s = Name s (Just ns) Nothing
1207−
12081172-- | Format as per RFC 1123.
12091173formatRFC1123 :: UTCTime -> T.Text
12101174formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1175+
1176+lookupRegionCache :: Bucket -> Minio (Maybe Region)
1177+lookupRegionCache b = do
1178+ rMVar <- asks mcRegionMap
1179+ rMap <- UM.readMVar rMVar
1180+ return $ H.lookup b rMap
1181+
1182+addToRegionCache :: Bucket -> Region -> Minio ()
1183+addToRegionCache b region = do
1184+ rMVar <- asks mcRegionMap
1185+ UM.modifyMVar_ rMVar $ return . H.insert b region
1186+
1187+deleteFromRegionCache :: Bucket -> Minio ()
1188+deleteFromRegionCache b = do
1189+ rMVar <- asks mcRegionMap
1190+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Errors.hs+2−1
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@ data MErrV
4949 | MErrVInvalidEncryptionKeyLength
5050 | MErrVStreamingBodyUnexpectedEOF
5151 | MErrVUnexpectedPayload
52+ | MErrVSTSEndpointNotFound
5253 deriving stock (Show, Eq)
5354
5455instance Exception MErrV
src/Network/Minio/PresignedOperations.hs+9−7
@@ -1,7 +1,5 @@
1−{-# LANGUAGE CPP #-}
2−
31--
4−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
53--
64-- Licensed under the Apache License, Version 2.0 (the "License");
75-- you may not use this file except in compliance with the License.
@@ -15,6 +13,7 @@
1513-- See the License for the specific language governing permissions and
1614-- limitations under the License.
1715--
16+{-# LANGUAGE CPP #-}
1817
1918module Network.Minio.PresignedOperations
2019 ( UrlExpiry,
@@ -39,7 +38,6 @@ where
3938
4039import Data.Aeson ((.=))
4140import qualified Data.Aeson as Json
42−import qualified Data.ByteArray as BA
4341import Data.ByteString.Builder (byteString, toLazyByteString)
4442import qualified Data.HashMap.Strict as H
4543import qualified Data.Text as T
@@ -48,6 +46,7 @@ import Lib.Prelude
4846import qualified Network.HTTP.Client as NClient
4947import qualified Network.HTTP.Types as HT
5048import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
5150import Network.Minio.Data
5251import Network.Minio.Data.Time
5352import Network.Minio.Errors
@@ -300,6 +299,8 @@ presignedPostPolicy ::
300299presignedPostPolicy p = do
301300 ci <- asks mcConnInfo
302301 signTime <- liftIO Time.getCurrentTime
302+ mgr <- asks mcConnManager
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
303304
304305 let extraConditions signParams =
305306 [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
@@ -308,7 +309,7 @@ presignedPostPolicy p = do
308309 "x-amz-credential"
309310 ( T.intercalate
310311 "/"
311− [ connectAccessKey ci,
312+ [ coerce $ cvAccessKey cv,
312313 decodeUtf8 $ credentialScope signParams
313314 ]
314315 )
@@ -319,8 +320,9 @@ presignedPostPolicy p = do
319320 }
320321 sp =
321322 SignParams
322− (connectAccessKey ci)
323− (BA.convert (encodeUtf8 $ connectSecretKey ci :: ByteString))
323+ (coerce $ cvAccessKey cv)
324+ (coerce $ cvSecretKey cv)
325+ (coerce $ cvSessionToken cv)
324326 ServiceS3
325327 signTime
326328 (Just $ connectRegion ci)
src/Network/Minio/S3API.hs+9−1
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,6 +14,14 @@
1414-- limitations under the License.
1515--
1616
17+-- |
18+-- Module: Network.Minio.S3API
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20+-- License: Apache 2.0
21+-- Maintainer: MinIO Dev Team <dev@min.io>
22+--
23+-- Lower-level API for S3 compatible object stores. Start with @Network.Minio@
24+-- and use this only if needed.
1725module Network.Minio.S3API
1826 ( Region,
1927 getLocation,
src/Network/Minio/Sign/V4.hs+21−31
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,7 +15,16 @@
1515--
1616{-# LANGUAGE BangPatterns #-}
1717
18−module Network.Minio.Sign.V4 where
18+module Network.Minio.Sign.V4
19+ ( SignParams (..),
20+ signV4QueryParams,
21+ signV4,
22+ signV4PostPolicy,
23+ signV4Stream,
24+ Service (..),
25+ credentialScope,
26+ )
27+where
1928
2029import qualified Conduit as C
2130import qualified Data.ByteArray as BA
@@ -23,6 +32,7 @@ import qualified Data.ByteString as B
2332import qualified Data.ByteString.Base64 as Base64
2433import qualified Data.ByteString.Char8 as B8
2534import qualified Data.ByteString.Lazy as LB
35+import Data.CaseInsensitive (mk)
2636import qualified Data.CaseInsensitive as CI
2737import qualified Data.HashMap.Strict as Map
2838import qualified Data.HashSet as Set
@@ -52,17 +62,6 @@ ignoredHeaders =
5262 H.hUserAgent
5363 ]
5464
55−data SignV4Data = SignV4Data
56− { sv4SignTime :: UTCTime,
57− sv4Scope :: ByteString,
58− sv4CanonicalRequest :: ByteString,
59− sv4HeadersToSign :: [(ByteString, ByteString)],
60− sv4Output :: [(ByteString, ByteString)],
61− sv4StringToSign :: ByteString,
62− sv4SigningKey :: ByteString
63− }
64− deriving stock (Show)
65−
6665data Service = ServiceS3 | ServiceSTS
6766 deriving stock (Eq, Show)
6867
@@ -73,6 +72,7 @@ toByteString ServiceSTS = "sts"
7372data SignParams = SignParams
7473 { spAccessKey :: Text,
7574 spSecretKey :: BA.ScrubbedBytes,
75+ spSessionToken :: Maybe BA.ScrubbedBytes,
7676 spService :: Service,
7777 spTimeStamp :: UTCTime,
7878 spRegion :: Maybe Text,
@@ -81,23 +81,6 @@ data SignParams = SignParams
8181 }
8282 deriving stock (Show)
8383
84−debugPrintSignV4Data :: SignV4Data -> IO ()
85−debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
86− B8.putStrLn "SignV4Data:"
87− B8.putStr "Timestamp: " >> print t
88− B8.putStr "Scope: " >> B8.putStrLn s
89− B8.putStrLn "Canonical Request:"
90− B8.putStrLn cr
91− B8.putStr "Headers to Sign: " >> print h2s
92− B8.putStr "Output: " >> print o
93− B8.putStr "StringToSign: " >> B8.putStrLn sts
94− B8.putStr "SigningKey: " >> printBytes sk
95− B8.putStrLn "END of SignV4Data ========="
96− where
97− printBytes b = do
98− mapM_ (\x -> B.putStr $ B.singleton x <> " ") $ B.unpack b
99− B8.putStrLn ""
100−
10184mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
10285mkAuthHeader accessKey scope signedHeaderKeys sign =
10386 let authValue =
@@ -116,6 +99,9 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
11699data IsStreaming = IsStreamingLength Int64 | NotStreaming
117100 deriving stock (Eq, Show)
118101
102+amzSecurityToken :: ByteString
103+amzSecurityToken = "X-Amz-Security-Token"
104+
119105-- | Given SignParams and request details, including request method,
120106-- request path, headers, query params and payload hash, generates an
121107-- updated set of headers, including the x-amz-date header and the
@@ -144,6 +130,7 @@ signV4QueryParams !sp !req =
144130 ("X-Amz-Expires", maybe "" showBS expiry),
145131 ("X-Amz-SignedHeaders", signedHeaderKeys)
146132 ]
133+ ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
147134 finalQP =
148135 parseQuery (NC.queryString req)
149136 ++ if isJust expiry
@@ -185,6 +172,7 @@ signV4 !sp !req =
185172 | spService sp == ServiceS3
186173 ]
187174 )
175+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
188176
189177 -- 1. compute canonical request
190178 reqHeaders = NC.requestHeaders req ++ extraHeaders
@@ -347,10 +335,11 @@ signV4PostPolicy !postPolicyJSON !sp =
347335 let stringToSign = Base64.encode postPolicyJSON
348336 signingKey = getSigningKey sp
349337 signature = computeSignature stringToSign signingKey
350− in Map.fromList
338+ in Map.fromList $
351339 [ ("x-amz-signature", signature),
352340 ("policy", stringToSign)
353341 ]
342+ ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
354343
355344chunkSizeConstant :: Int
356345chunkSizeConstant = 64 * 1024
@@ -401,6 +390,7 @@ signV4Stream !payloadLength !sp !req =
401390 ("content-length", showBS signedContentLength),
402391 ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
403392 ]
393+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
404394 requestHeaders =
405395 addContentEncoding $
406396 foldr setHeader (NC.requestHeaders req) extraHeaders
src/Network/Minio/Utils.hs+26−44
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -24,7 +24,6 @@ import qualified Data.ByteString.Lazy as LB
2424import Data.CaseInsensitive (mk, original)
2525import qualified Data.Conduit.Binary as CB
2626import qualified Data.HashMap.Strict as H
27−import qualified Data.List as List
2827import qualified Data.Text as T
2928import Data.Text.Read (decimal)
3029import Data.Time
@@ -37,14 +36,12 @@ import Network.HTTP.Conduit (Response)
3736import qualified Network.HTTP.Conduit as NC
3837import qualified Network.HTTP.Types as HT
3938import qualified Network.HTTP.Types.Header as Hdr
40−import Network.Minio.Data
4139import Network.Minio.Data.ByteString
4240import Network.Minio.JsonParser (parseErrResponseJSON)
43−import Network.Minio.XmlParser (parseErrResponse)
41+import Network.Minio.XmlCommon (parseErrResponse)
4442import qualified System.IO as IO
4543import qualified UnliftIO as U
4644import qualified UnliftIO.Async as A
47−import qualified UnliftIO.MVar as UM
4845
4946allocateReadFile ::
5047 (MonadUnliftIO m, R.MonadResource m) =>
@@ -115,6 +112,16 @@ getMetadata :: [HT.Header] -> [(Text, Text)]
115112getMetadata =
116113 map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
117114
115+-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
116+-- stripped and a Just is returned.
117+userMetadataHeaderNameMaybe :: Text -> Maybe Text
118+userMetadataHeaderNameMaybe k =
119+ let prefix = T.toCaseFold "X-Amz-Meta-"
120+ n = T.length prefix
121+ in if T.toCaseFold (T.take n k) == prefix
122+ then Just (T.drop n k)
123+ else Nothing
124+
118125toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
119126toMaybeMetadataHeader (k, v) =
120127 (,v) <$> userMetadataHeaderNameMaybe k
@@ -128,6 +135,14 @@ getNonUserMetadataMap =
128135 . fst
129136 )
130137
138+addXAmzMetaPrefix :: Text -> Text
139+addXAmzMetaPrefix s
140+ | isJust (userMetadataHeaderNameMaybe s) = s
141+ | otherwise = "X-Amz-Meta-" <> s
142+
143+mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
144+mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
145+
131146-- | This function collects all headers starting with `x-amz-meta-`
132147-- and strips off this prefix, and returns a map.
133148getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
@@ -135,6 +150,12 @@ getUserMetadataMap =
135150 H.fromList
136151 . mapMaybe toMaybeMetadataHeader
137152
153+getHostHeader :: (ByteString, Int) -> ByteString
154+getHostHeader (host_, port_) =
155+ if port_ == 80 || port_ == 443
156+ then host_
157+ else host_ <> ":" <> show port_
158+
138159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
139160getLastModifiedHeader hs = do
140161 modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
@@ -262,42 +283,3 @@ chunkBSConduit (s : ss) = do
262283 | B.length bs == s -> C.yield bs >> chunkBSConduit ss
263284 | B.length bs > 0 -> C.yield bs
264285 | otherwise -> return ()
265−
266−-- | Select part sizes - the logic is that the minimum part-size will
267−-- be 64MiB.
268−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
269−selectPartSizes size =
270− uncurry (List.zip3 [1 ..]) $
271− List.unzip $
272− loop 0 size
273− where
274− ceil :: Double -> Int64
275− ceil = ceiling
276− partSize =
277− max
278− minPartSize
279− ( ceil $
280− fromIntegral size
281− / fromIntegral maxMultipartParts
282− )
283− m = partSize
284− loop st sz
285− | st > sz = []
286− | st + m >= sz = [(st, sz - st)]
287− | otherwise = (st, m) : loop (st + m) sz
288−
289−lookupRegionCache :: Bucket -> Minio (Maybe Region)
290−lookupRegionCache b = do
291− rMVar <- asks mcRegionMap
292− rMap <- UM.readMVar rMVar
293− return $ H.lookup b rMap
294−
295−addToRegionCache :: Bucket -> Region -> Minio ()
296−addToRegionCache b region = do
297− rMVar <- asks mcRegionMap
298− UM.modifyMVar_ rMVar $ return . H.insert b region
299−
300−deleteFromRegionCache :: Bucket -> Minio ()
301−deleteFromRegionCache b = do
302− rMVar <- asks mcRegionMap
303− UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/XmlCommon.hsnew file+65
@@ -0,0 +1,65 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.XmlCommon where
18+
19+import qualified Data.Text as T
20+import Data.Text.Read (decimal)
21+import Data.Time (UTCTime)
22+import Data.Time.Format.ISO8601 (iso8601ParseM)
23+import Lib.Prelude (throwIO)
24+import Network.Minio.Errors
25+import Text.XML (Name (Name), def, parseLBS)
26+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
27+
28+s3Name :: Text -> Text -> Name
29+s3Name ns s = Name s (Just ns) Nothing
30+
31+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
32+uncurry4 f (a, b, c, d) = f a b c d
33+
34+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
35+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
36+
37+-- | Parse time strings from XML
38+parseS3XMLTime :: MonadIO m => Text -> m UTCTime
39+parseS3XMLTime t =
40+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
41+ iso8601ParseM $
42+ toString t
43+
44+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
45+parseDecimal numStr =
46+ either (throwIO . MErrVXmlParse . show) return $
47+ fst <$> decimal numStr
48+
49+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
50+parseDecimals numStr = forM numStr parseDecimal
51+
52+s3Elem :: Text -> Text -> Axis
53+s3Elem ns = element . s3Name ns
54+
55+parseRoot :: (MonadIO m) => LByteString -> m Cursor
56+parseRoot =
57+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
58+ . parseLBS def
59+
60+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
61+parseErrResponse xmldata = do
62+ r <- parseRoot xmldata
63+ let code = T.concat $ r $/ laxElement "Code" &/ content
64+ message = T.concat $ r $/ laxElement "Message" &/ content
65+ return $ toServiceErr code message
src/Network/Minio/XmlGenerator.hs+2−1
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ where
2525import qualified Data.ByteString.Lazy as LBS
2626import qualified Data.Text as T
2727import Network.Minio.Data
28+import Network.Minio.XmlCommon
2829import Text.XML
2930
3031-- | Create a bucketConfig request body XML
src/Network/Minio/XmlParser.hs+2−144
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -27,54 +27,18 @@ module Network.Minio.XmlParser
2727 parseErrResponse,
2828 parseNotification,
2929 parseSelectProgress,
30− parseSTSAssumeRoleResult,
3130 )
3231where
3332
34−import qualified Data.ByteArray as BA
3533import qualified Data.ByteString.Lazy as LB
3634import qualified Data.HashMap.Strict as H
3735import Data.List (zip4, zip6)
3836import qualified Data.Text as T
39−import Data.Text.Read (decimal)
4037import Data.Time
41−import Data.Time.Format.ISO8601 (iso8601ParseM)
42−import Lib.Prelude
4338import Network.Minio.Data
44−import Network.Minio.Errors
45−import Text.XML
39+import Network.Minio.XmlCommon
4640import Text.XML.Cursor hiding (bool)
4741
48−-- | Helper functions.
49−uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
50−uncurry4 f (a, b, c, d) = f a b c d
51−
52−uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
53−uncurry6 f (a, b, c, d, e, g) = f a b c d e g
54−
55−-- | Parse time strings from XML
56−parseS3XMLTime :: MonadIO m => Text -> m UTCTime
57−parseS3XMLTime t =
58− maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
59− iso8601ParseM $
60− toString t
61−
62−parseDecimal :: (MonadIO m, Integral a) => Text -> m a
63−parseDecimal numStr =
64− either (throwIO . MErrVXmlParse . show) return $
65− fst <$> decimal numStr
66−
67−parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
68−parseDecimals numStr = forM numStr parseDecimal
69−
70−s3Elem :: Text -> Text -> Axis
71−s3Elem ns = element . s3Name ns
72−
73−parseRoot :: (MonadIO m) => LByteString -> m Cursor
74−parseRoot =
75− either (throwIO . MErrVXmlParse . show) (return . fromDocument)
76− . parseLBS def
77−
7842-- | Parse the response XML of a list buckets call.
7943parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
8044parseListBuckets xmldata = do
@@ -219,13 +183,6 @@ parseListPartsResponse xmldata = do
219183
220184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
221185
222−parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
223−parseErrResponse xmldata = do
224− r <- parseRoot xmldata
225− let code = T.concat $ r $/ laxElement "Code" &/ content
226− message = T.concat $ r $/ laxElement "Message" &/ content
227− return $ toServiceErr code message
228−
229186parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
230187parseNotification xmldata = do
231188 r <- parseRoot xmldata
@@ -271,102 +228,3 @@ parseSelectProgress xmldata = do
271228 <$> parseDecimal bScanned
272229 <*> parseDecimal bProcessed
273230 <*> parseDecimal bReturned
274−
275−-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
276−-- <AssumeRoleResult>
277−-- <SourceIdentity>Alice</SourceIdentity>
278−-- <AssumedRoleUser>
279−-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
280−-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
281−-- </AssumedRoleUser>
282−-- <Credentials>
283−-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
284−-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
285−-- <SessionToken>
286−-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
287−-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
288−-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
289−-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
290−-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
291−-- </SessionToken>
292−-- <Expiration>2019-11-09T13:34:41Z</Expiration>
293−-- </Credentials>
294−-- <PackedPolicySize>6</PackedPolicySize>
295−-- </AssumeRoleResult>
296−-- <ResponseMetadata>
297−-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
298−-- </ResponseMetadata>
299−-- </AssumeRoleResponse>
300−
301−parseSTSAssumeRoleResult :: MonadIO m => ByteString -> Text -> m AssumeRoleResult
302−parseSTSAssumeRoleResult xmldata namespace = do
303− r <- parseRoot $ LB.fromStrict xmldata
304− let s3Elem' = s3Elem namespace
305− sourceIdentity =
306− T.concat $
307− r
308− $/ s3Elem' "AssumeRoleResult"
309− &/ s3Elem' "SourceIdentity"
310− &/ content
311− roleArn =
312− T.concat $
313− r
314− $/ s3Elem' "AssumeRoleResult"
315− &/ s3Elem' "AssumedRoleUser"
316− &/ s3Elem' "Arn"
317− &/ content
318− roleId =
319− T.concat $
320− r
321− $/ s3Elem' "AssumeRoleResult"
322− &/ s3Elem' "AssumedRoleUser"
323− &/ s3Elem' "AssumedRoleId"
324− &/ content
325−
326− convSB :: Text -> BA.ScrubbedBytes
327− convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
328−
329− credsInfo = do
330− cr <-
331− maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
332− listToMaybe $
333− r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
334− let cur = fromNode $ node cr
335− return
336− ( CredentialValue
337− { cvAccessKey =
338− coerce $
339− T.concat $
340− cur $/ s3Elem' "AccessKeyId" &/ content,
341− cvSecretKey =
342− coerce $
343− convSB $
344− T.concat $
345− cur
346− $/ s3Elem' "SecretAccessKey"
347− &/ content,
348− cvSessionToken =
349− Just $
350− coerce $
351− convSB $
352− T.concat $
353− cur
354− $/ s3Elem' "SessionToken"
355− &/ content
356− },
357− T.concat $ cur $/ s3Elem' "Expiration" &/ content
358− )
359− creds <- either throwIO pure credsInfo
360− expiry <- parseS3XMLTime $ snd creds
361− let roleCredentials =
362− AssumeRoleCredentials
363− { arcCredentials = fst creds,
364− arcExpiration = expiry
365− }
366− return
367− AssumeRoleResult
368− { arrSourceIdentity = sourceIdentity,
369− arrAssumedRoleArn = roleArn,
370− arrAssumedRoleId = roleId,
371− arrRoleCredentials = roleCredentials
372− }
test/LiveServer.hs+67−9
@@ -1,7 +1,5 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−
31--
4−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
53--
64-- Licensed under the Apache License, Version 2.0 (the "License");
75-- you may not use this file except in compliance with the License.
@@ -32,6 +30,7 @@ import qualified Network.HTTP.Client.MultipartFormData as Form
3230import qualified Network.HTTP.Conduit as NC
3331import qualified Network.HTTP.Types as HT
3432import Network.Minio
33+import Network.Minio.Credentials (Creds (CredsStatic))
3534import Network.Minio.Data
3635import Network.Minio.Data.Crypto
3736import Network.Minio.S3API
@@ -77,15 +76,35 @@ mkRandFile size = do
7776funTestBucketPrefix :: Text
7877funTestBucketPrefix = "miniohstest-"
7978
80−loadTestServer :: IO ConnectInfo
81−loadTestServer = do
79+loadTestServerConnInfo :: IO ConnectInfo
80+loadTestServerConnInfo = do
8281 val <- Env.lookupEnv "MINIO_LOCAL"
8382 isSecure <- Env.lookupEnv "MINIO_SECURE"
8483 return $ case (val, isSecure) of
85− (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
86− (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
84+ (Just _, Just _) -> setCreds (CredentialValue "minio" "minio123" mempty) "https://localhost:9000"
85+ (Just _, Nothing) -> setCreds (CredentialValue "minio" "minio123" mempty) "http://localhost:9000"
8786 (Nothing, _) -> minioPlayCI
8887
88+loadTestServerConnInfoSTS :: IO ConnectInfo
89+loadTestServerConnInfoSTS = do
90+ val <- Env.lookupEnv "MINIO_LOCAL"
91+ isSecure <- Env.lookupEnv "MINIO_SECURE"
92+ let cv = CredentialValue "minio" "minio123" mempty
93+ assumeRole =
94+ STSAssumeRole
95+ { sarCredentials = cv,
96+ sarOptions = defaultSTSAssumeRoleOptions
97+ }
98+ case (val, isSecure) of
99+ (Just _, Just _) -> setSTSCredential assumeRole "https://localhost:9000"
100+ (Just _, Nothing) -> setSTSCredential assumeRole "http://localhost:9000"
101+ (Nothing, _) -> do
102+ cv' <- case connectCreds minioPlayCI of
103+ CredsStatic c -> return c
104+ _ -> error "unexpected play creds"
105+ let assumeRole' = assumeRole {sarCredentials = cv'}
106+ setSTSCredential assumeRole' minioPlayCI
107+
89108funTestWithBucket ::
90109 TestName ->
91110 (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
@@ -95,7 +114,7 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
95114 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
96115 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
97116 liftStep = liftIO . step
98− connInfo <- loadTestServer
117+ connInfo <- loadTestServerConnInfo
99118 ret <- runMinio connInfo $ do
100119 liftStep $ "Creating bucket for test - " ++ t
101120 foundBucket <- bucketExists b
@@ -105,6 +124,17 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
105124 deleteBucket b
106125 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
107126
127+ connInfoSTS <- loadTestServerConnInfoSTS
128+ let t' = t ++ " (with AssumeRole Credentials)"
129+ ret' <- runMinio connInfoSTS $ do
130+ liftStep $ "Creating bucket for test - " ++ t'
131+ foundBucket <- bucketExists b
132+ liftIO $ foundBucket @?= False
133+ makeBucket b Nothing
134+ minioTest liftStep b
135+ deleteBucket b
136+ isRight ret' @? ("Functional test " ++ t' ++ " failed => " ++ show ret')
137+
108138liveServerUnitTests :: TestTree
109139liveServerUnitTests =
110140 testGroup
@@ -125,7 +155,8 @@ liveServerUnitTests =
125155 presignedUrlFunTest,
126156 presignedPostPolicyFunTest,
127157 bucketPolicyFunTest,
128− getNPutSSECTest
158+ getNPutSSECTest,
159+ assumeRoleRequestTest
129160 ]
130161
131162basicTests :: TestTree
@@ -1187,3 +1218,30 @@ getNPutSSECTest =
11871218 step "Cleanup"
11881219 deleteObject bucket obj
11891220 else step "Skipping encryption test as server is not using TLS"
1221+
1222+assumeRoleRequestTest :: TestTree
1223+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1224+ step "Load credentials"
1225+ val <- Env.lookupEnv "MINIO_LOCAL"
1226+ isSecure <- Env.lookupEnv "MINIO_SECURE"
1227+ let localMinioCred = Just $ CredentialValue "minio" "minio123" mempty
1228+ playCreds =
1229+ case connectCreds minioPlayCI of
1230+ CredsStatic c -> Just c
1231+ _ -> Nothing
1232+ (cvMay, loc) =
1233+ case (val, isSecure) of
1234+ (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1235+ (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1236+ (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1237+ cv <- maybe (assertFailure "bad creds") return cvMay
1238+ let assumeRole =
1239+ STSAssumeRole cv $
1240+ defaultSTSAssumeRoleOptions
1241+ { saroLocation = Just "us-east-1",
1242+ saroEndpoint = Just loc
1243+ }
1244+ step "AssumeRole request"
1245+ res <- requestSTSCredential assumeRole
1246+ let v = credentialValueText $ fst res
1247+ print (v, snd res)
test/Spec.hs+1−2
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import Lib.Prelude
2020import Network.Minio.API.Test
2121import Network.Minio.CopyObject
2222import Network.Minio.Data
23−import Network.Minio.PutObject
2423import Network.Minio.Utils.Test
2524import Network.Minio.XmlGenerator.Test
2625import Network.Minio.XmlParser.Test